os module
The sandbox exposes a small, host-mediated subset of os. Filesystem
functions route through the same OS-call mechanism as pathlib and
open() (see pathlib.md, open.md): the host’s mount table (or os callback) decides
whether each call is permitted.
os.getenv(key, default=None)— yields to the host; the host decides which environment variables are visible (typically a curated subset, not the full host environment).os.environ— property that yields to the host and returns adictof the same curated environment. It is a plain dict, not anos._Environobject: mutating it does not propagate back to the host.os.listdir(path=None)— returns a list of entry names.os.scandir(path=None)—DirEntryobjects withname,path,is_dir(),is_file(),is_symlink(),is_junction(),stat()and__fspath__(); the iterator supportswithandclose().os.walk(top, topdown=True, onerror=None, followlinks=False)— including pruning by mutatingdirnames.os.stat(path)— returns the same 10-field stat result asPath.stat().os.mkdir(path, mode=0o777),os.makedirs(name, mode=0o777, exist_ok=False)os.remove(path),os.unlink(path),os.rmdir(path)os.rename(src, dst),os.replace(src, dst)os.urandom(size)— yields to the host, which must return exactlysizebytes; any other length, or a non-bytesvalue, raisesRuntimeError. Sandboxed code choosessize, so a host handler that allocates must cap it. Python’sAbstractOS.urandom()raisesMemoryErrorbefore allocating whensizeexceedsmax_urandom_bytes, 1 MiB by default;OSAccess(max_urandom_bytes=...)sets it, and zero rejects every nonempty request. Therandommodule makes this call only underrandom_start='call_host'; by default an unseeded generator seeds itself from OS entropy inside the sandbox (see random.md).os.fspath(path)— pure, no host involvement.os.getcwd()— pure: the sandbox’s virtual working directory.os.getcwdb()— pure: the same directory as UTF-8 bytes (virtual paths are always UTF-8, so no filesystem encoding is involved).os.chdir(path)— validated through aPath.stathost call (see below).- Constants (fixed POSIX values on every host OS, matching the sandbox’s
POSIX-only path model):
os.sep == '/',os.altsep is None,os.extsep == '.',os.curdir == '.',os.pardir == '..',os.linesep == '\n',os.name == 'posix',os.devnull == '/dev/null'.
- No
bytespaths. Paths must bestrorpathlib.Path.bytespaths raise the path-converterTypeErrorwithbytesdropped from the accepted-types phrase, e.g.stat: path should be string, os.PathLike or integer, not bytes, andos.walk’sfspathwording narrows the same way (expected str or os.PathLike object, not bytes). For every other rejected type the phrase is CPython’s verbatim, soos.stat(1.5)still saysshould be string, bytes, os.PathLike or integer. Noteopen()does acceptbytespaths, decoding them as UTF-8; theosfunctions do not. The verbatimos.listdirphrase is POSIX CPython’s, which includesintegereven though Windows CPython omits it (no fd-based listdir there). - Every file descriptor is closed. Where CPython’s converter takes an int as
an fd (
os.stat,os.listdir,os.chdir, and theos.pathfunctions built onos.stat), Monty takes it too, but the sandbox has no open fds, so the call fails as it would on a closed one:OSError: [Errno 9] Bad file descriptor: 3fromos.stat(3)(unnamed fromos.listdir, whosefdopendirroute names no file),Falsefromos.path.exists(3),isfileandisdir. This holds for the fds CPython would find open, including the stdio fds and bools (os.stat(True)is CPython’sos.stat(1), with aRuntimeWarning), and for negative ints, which CPython reports with a platform errno. Ints outside Cintraise the converter’sOverflowError(fd is greater than maximum) like CPython.os.stat(fd, follow_symlinks=False)fails on the fd before CPython’sValueErrorabout combining the two. - No
__fspath__protocol.os.fspath(and every path-taking function) accepts onlystr,bytes(fspath only), andpathlib.Path: a user-defined class implementing__fspath__raisesTypeErrorinstead of having its method called. dir_fdkeywords (dir_fd,src_dir_fd,dst_dir_fd) are parsed for signature parity, but any non-Nonevalue raises theNotImplementedErrorCPython uses on platforms without them (dir_fd unavailable on this platform). Non-int values raise the converterTypeError(argument should be integer or None, not str).os.stat(..., follow_symlinks=...)raisesNotImplementedError: stat: follow_symlinks unavailable on this platformfor any falsy value. CPython truth-tests the argument, soFalse,Noneand0all mean “lstat”, which Monty has no behavior for.os.lstatitself is not implemented.- All-keyword calls that overflow the signature are not always reported
the way CPython reports them.
os.fspath(path='a', foo=1)andos.listdir(path='.', foo=1)match (takes at most 1 keyword argument (2 given)), but functions with keyword-only slots (os.stat,os.mkdir,os.remove,os.rmdir,os.rename) report the first unknown keyword (stat() got an unexpected keyword argument 'foo') where CPython reports the arity (stat() takes at most 3 keyword arguments (4 given)). - The working directory is virtual and belongs to the session. A
session’s first feed sets it (an explicit
cwd, else that feed’s first mount’s virtual path, else/); it then persists across feeds, including anyos.chdir(), until a feed passescwdagain.os.getcwd()reports it and relative paths are resolved against it inside the interpreter, so a mount oroscallback only ever sees absolute paths. Host errors therefore name the resolved path (open('missing')raises[Errno 2] No such file or directory: '/data/missing') where CPython names the argument as written. Absolute paths reach mounts as written. Joining preserves.and..so mounts can validate NUL bytes and path limits before normalization. Python and JavaScriptoscallbacks receive lexically normalized paths, including both rename arguments. The interpreter rejects NUL bytes before dispatch, even in components cancelled by... Existence predicates returnFalsefor these paths; other operations raiseValueError. Length and depth limits are mount policy: a feed with any mount applies them to every path before the callback sees it, including paths no mount covers, while a feed with no mounts passes paths of any length to the callback. os.chdir(path)suspends asPath.staton the resolved target: hosts cannot observe a directory change, and without a mount oroshandler it raisesPermissionError. The interpreter raisesNotADirectoryErrorwhen the reply is not a directory, naming the argument as written like CPython;FileNotFoundErrorcomes from the host and names the resolved path.os.chdir('')raisesFileNotFoundErrorwithout consulting the host. Only after the host accepts the target is the stored directory lexically normalized (..collapses without consulting symlinks). An integer is a closed fd ([Errno 9] Bad file descriptor); CPython wouldfchdir. A Rust host that answers the stat with a future getsRuntimeErrorinstead of a silently unchanged directory.modearguments are type-checked ('str' object cannot be interpreted as an integer) but otherwise ignored: Monty’s filesystem backends do not model POSIX permission bits.os.replaceis an alias ofos.renameat the host boundary: both suspend with the same rename OS call, so overwrite semantics are whatever the host backend does (POSIX rename overwrites; a Windows host may refuse). CPython’sos.replaceguarantees overwrite on all platforms.- Hosts see pathlib-style call names.
os.listdirsuspends asPath.iterdir(the interpreter reduces the returned paths to names),os.statandos.chdirasPath.stat,os.remove/os.unlinkasPath.unlink,os.mkdir/os.makedirsasPath.mkdir,os.rename/os.replaceasPath.rename. A customoscallback cannot distinguish e.g.os.listdirfromPath.iterdir. os.scandirandos.walkread their whole directory or tree when called. One host call answers each (see pathlib.md for the shared divergences: no laziness, sorted entries, unreadable subdirectories read as empty). Soos.walk(1.5)raises itsTypeErrorat the call, where CPython’s generator raises at the firstnext().DirEntryanswers from the scan.is_dir(),is_file()andis_symlink()never ask the host again;stat()does, andstat(follow_symlinks=False)on a symlink raises the sameNotImplementedErrorasos.stat.inode()is not implemented andis_junction()is alwaysFalse.DirEntry,Path(entry),open(entry)andos.fspath(entry)accept it as a path, but otheros.PathLikeobjects are still refused (see above).- Hosts see one call name for every directory scan.
os.scandir,os.walk,Path.walk,Path.globandPath.rgloball suspend asPath.scan. os.statresults print asStatResult(...), notos.stat_result(...), and carry only the 10 core fields, same asPath.stat()(see filesystem.md).- Error side-effects differ slightly for
os.makedirs: Monty validatesmodeup front, while CPython only fails when it reaches the finalmkdir, after creating parent directories.
os.path is posixpath on every host, including Windows, where CPython’s is ntpath; import posixpath yields
the same module.
The host-backed functions (exists, isfile, isdir, islink, lexists, ismount, samefile, getsize,
getmtime, getatime, getctime, realpath) take paths like the os functions above: str or pathlib.Path,
no bytes. The os.stat-based ones (exists, isfile, isdir, samefile, getsize, getmtime, getatime,
getctime) treat an int as a closed fd, like CPython; islink, lexists, ismount and realpath reject it with
CPython’s TypeError. The pure functions, expanduser and expandvars take bytes too, as CPython’s do.
ismountreports every existing path as a mount point. The sandbox cannot see where the host’s mounts begin, andTrueis what keeps “walk up until a mount point” loops terminating.samefilecompares(st_ino, st_dev)like CPython, but mounts report both as0for every file, so for two mount paths the normalized virtual paths decide instead and hard links are not detected.samestatcompares the same two fields, so two mount stat results always compare equal.samestatreadsst_inoandst_devsynchronously, so on a host-backed object a lazy attribute reads as absent and raisesAttributeError(see classes.md), where CPython would evaluate the getter.realpathresolves lexically on mounts (see filesystem.md), so a missing path does not raise unlessstrictis true, which then raisesFileNotFoundErrornaming the resolved path. TheNotADirectoryErrorand symlink-loopOSErrorof CPython’s strict mode never occur, andALLOW_MISSINGdoes not exist.expanduserconsults only$HOME, through the host: without it the path is returned unchanged, and~useris always returned unchanged, since the sandbox has no password database to fall back on.expandvarsreadsos.environthrough the host, ignoring entries whose key or value is notstr. Abytespath is matched against that samestrenvironment encoded as UTF-8, where CPython readsos.environb.commonprefixon lists whose elements cannot be ordered reports the lists ('<' not supported between instances of 'list' and 'list') where CPython names the elements; this is Monty’s general list-comparison wording.sameopenfileis not implemented: there are no file descriptors tofstat.supports_unicode_filenamesis alwaysFalse(CPython sets it on macOS only).
Everything else, including but not limited to: os.fchdir, os.fwalk,
os.removedirs, os.renames, os.lstat, os.access, os.symlink,
os.readlink, os.link, os.chmod, os.chown, os.umask, os.truncate,
os.utime, os.system, os.popen, os.fork, os.exec*, os.spawn*,
os.kill, os.pipe, os.read, os.write, os.open, os.close,
os.dup, os.fsync, os.cpu_count, os.getpid,
os.getuid, os.getgid, os.uname, os.terminal_size, os.get_terminal_size.
subprocess, signal, socket, threading, multiprocessing are not
importable either (see modules.md).