Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.mesa.dev/llms.txt

Use this file to discover all available pages before exploring further.

The Mesa Python SDK lets you mount repositories as a virtual filesystem. Read files, write files, run shell commands — without cloning anything to disk. Everything is async. The SDK connects to Mesa’s VCS backend over the network, so you always work with the latest data. For installation and client setup, see the Python SDK overview.

Mounting a Filesystem

Use mesa.fs.mount() to mount one or more repos. It’s an async context manager that handles setup and teardown automatically:
from mesa_sdk import Mesa

async with Mesa() as mesa:
    async with mesa.fs.mount(repos=["my-repo"]) as fs:
        data = await fs.read("/my-repo/README.md")
        print(data.decode())
Under the hood, mount():
  1. Mints a scoped API key with the minimum required permissions
  2. Connects to the Mesa VCS backend
  3. Yields a MesaFileSystem you can use for I/O
When the async with block exits, the scoped key is automatically revoked. It also carries a 1-hour server-side TTL, so even if revocation fails, it expires on its own.

Mount options

from mesa_sdk import RepoConfig, DiskCacheConfig

async with mesa.fs.mount(
    repos=[
        "simple-repo",                                  # default bookmark
        RepoConfig("pinned-repo", bookmark="staging"),   # specific bookmark
        RepoConfig("frozen-repo", change_id="abc123"),   # specific change
    ],
    mode="rw",                                           # or "ro" for read-only
    disk_cache=DiskCacheConfig("/tmp/mesa-cache"),        # optional on-disk cache
) as fs:
    ...
ParameterTypeDefaultDescription
reposlist[str | RepoConfig]requiredRepos to mount. Pass a name or a RepoConfig for pinning.
mode"ro" | "rw""rw"Access mode. Read-only mounts can’t write.
disk_cacheDiskCacheConfig | NoneNoneOn-disk cache location and optional size cap.

Paths

Every path starts with a leading slash and the repo name:
/my-repo/src/main.py
/my-repo/README.md

Reading Files

# Raw bytes
raw = await fs.read("/my-repo/image.png")

# Text — decode the bytes yourself
text = (await fs.read("/my-repo/README.md")).decode()
There is no read_text method. The SDK returns raw bytes so you control the encoding. Decode with .decode() or .decode("utf-8").

Writing Files

# Create or overwrite a file
await fs.write("/my-repo/output.txt", b"Hello!")

# Append to a file (creates it if missing)
await fs.append("/my-repo/log.txt", b"new line\n")
Parent directories must exist. Create them first if needed:
await fs.mkdir("/my-repo/src/utils", recursive=True)
await fs.write("/my-repo/src/utils/helpers.py", b"def greet(): ...")

Checking Existence

if await fs.exists("/my-repo/config.yaml"):
    config = await fs.read("/my-repo/config.yaml")

Listing Directories

entries = await fs.readdir("/my-repo/src")
# ["main.py", "utils.py", "tests"]
Order is not guaranteed. Sort the result if you need deterministic output.

File Metadata

stat returns an FsStat object with metadata about a file or directory:
info = await fs.stat("/my-repo/src/main.py")
print(info.size)         # bytes
print(info.is_file)      # True
print(info.is_directory) # False
print(info.mode)         # e.g. 0o100644
print(info.mtime_ms)     # milliseconds since epoch
Use lstat to inspect a symlink itself (without following it):
link_info = await fs.lstat("/my-repo/latest")
print(link_info.is_symbolic_link)  # True
FieldTypeDescription
is_fileboolRegular file
is_directoryboolDirectory
is_symbolic_linkboolSymlink (always False from stat)
modeintPermission bits
sizeintSize in bytes
mtime_msfloatModification time (milliseconds since epoch)

File Operations

# Copy a file
await fs.cp("/my-repo/a.txt", "/my-repo/b.txt")

# Copy a directory
await fs.cp("/my-repo/src", "/my-repo/src-backup", recursive=True)

# Move / rename
await fs.mv("/my-repo/old.txt", "/my-repo/new.txt")

# Delete a file
await fs.rm("/my-repo/temp.txt")

# Delete a directory (recursive required)
await fs.rm("/my-repo/build", recursive=True)

# Delete if exists (no error when missing)
await fs.rm("/my-repo/maybe.txt", force=True)

# Create directories
await fs.mkdir("/my-repo/a/b/c", recursive=True)
# Create a symlink
await fs.symlink("../shared/config.yaml", "/my-repo/config.yaml")

# Read the target
target = await fs.readlink("/my-repo/config.yaml")

# Resolve through all symlinks to an absolute path
real = await fs.realpath("/my-repo/config.yaml")
Hard links are not supported. Calling fs.link() raises NotImplementedError.

Permissions and Timestamps

# Set permission bits
await fs.chmod("/my-repo/script.sh", 0o755)

# Set access and modification time
import time
now_ms = time.time() * 1000
await fs.utimes("/my-repo/file.txt", atime_ms=now_ms, mtime_ms=now_ms)
utimes takes milliseconds since the Unix epoch, not seconds. Multiply time.time() by 1000.

Running Shell Commands

fs.bash() creates a Bash interpreter that runs commands against the mounted filesystem. No host shell is spawned — it’s fully isolated.
bash = fs.bash(cwd="/my-repo", env={"CI": "true"})
result = await bash.exec("grep -r TODO src/ | wc -l")
print(result.stdout.decode().strip())

Bash options

ParameterTypeDefaultDescription
envdict[str, str] | NoneemptyEnvironment variables. Host env is not inherited.
cwdstr | None"/"Working directory inside the mount.
timeout_msint | None30000Per-exec wall-clock timeout in milliseconds.

ExecResult

bash.exec() returns an ExecResult:
FieldTypeDescription
stdoutbytesStandard output
stderrbytesStandard error
exit_codeintExit code (0 = success)

Pipelines

Shell pipes, redirects, and multi-line scripts all work:
result = await bash.exec("""
    find /my-repo -name '*.py' | \
    xargs grep -l 'TODO' | \
    wc -l
""")
todo_count = int(result.stdout.strip())

Binary files

If you cat a binary (non-UTF-8) file, the command fails with a non-zero exit code and stderr tells you to use read instead. This is intentional — use fs.read() for binary data.

Changes

Changes are Mesa’s equivalent of commits. The filesystem exposes change operations through fs.changes:
# Create a new change forked from a bookmark
change_id = await fs.changes.new("my-repo", bookmark="main")

# Make edits on this change
await fs.write("/my-repo/feature.py", b"def new_feature(): ...")

# Fork from an existing change
child_id = await fs.changes.new("my-repo", change_id=change_id)

# Check out an existing change (doesn't create a new one)
await fs.changes.edit("my-repo", change_id=change_id)

# List recent changes
changes = await fs.changes.list("my-repo", limit=10)
for ch in changes:
    print(f"change={ch.change_id} commit={ch.commit_oid}")
MethodReturnsDescription
new(repo, bookmark=, change_id=)strCreate a new change, returns its hex ID
edit(repo, bookmark=, change_id=)strCheck out an existing change
list(repo, limit=50)list[ChangeInfo]List recent changes

Bookmarks

Bookmarks are named references — like git branches. The filesystem exposes read and create operations:
# List all bookmarks
bookmarks = await fs.bookmarks.list("my-repo")
# ["main", "staging", "feature-x"]

# Create a bookmark at the current change's commit
await fs.bookmarks.create("my-repo", "my-feature")
For merging and moving bookmarks, use the REST API: mesa.bookmarks.merge(...) and mesa.bookmarks.move(...).

Disk Caching

By default the mount uses an in-memory cache only. For long-running processes (CI pipelines, agents, notebooks), enable on-disk caching:
from mesa_sdk import DiskCacheConfig

async with mesa.fs.mount(
    repos=["my-repo"],
    disk_cache=DiskCacheConfig("/tmp/mesa-cache", max_size_bytes=500_000_000),
) as fs:
    # First read: fetched from network, cached to disk
    # Subsequent reads: served from cache
    data = await fs.read("/my-repo/large-dataset.csv")

Error Handling

Filesystem operations raise Python’s built-in exceptions — not MesaError:
try:
    await fs.read("/my-repo/missing.txt")
except FileNotFoundError:
    print("File doesn't exist")

try:
    await fs.read("/my-repo/src")
except IsADirectoryError:
    print("That's a directory, use readdir()")

try:
    await fs.write("/my-repo/file.txt", b"data")
except PermissionError:
    print("Mount is read-only")
ExceptionWhen
FileNotFoundErrorPath does not exist
FileExistsErrorAlready exists (e.g. duplicate bookmark)
IsADirectoryErrorExpected a file, got a directory
NotADirectoryErrorExpected a directory, got a file
PermissionErrorWrite on a read-only mount
OSErrorGeneral I/O failure
NotImplementedErrorlink() — hard links not supported

Multiprocessing

The filesystem is not fork-safe. If you use multiprocessing, set the start method before creating any Mesa objects:
import multiprocessing
multiprocessing.set_start_method("spawn")  # or "forkserver"

Complete Example

A script that creates a change, writes files, runs tests, and prints the result:
import asyncio
from mesa_sdk import Mesa

async def main():
    async with Mesa() as mesa:
        async with mesa.fs.mount(repos=["my-app"]) as fs:
            # Create a change to work on
            change_id = await fs.changes.new("my-app", bookmark="main")

            # Write a new module
            await fs.mkdir("/my-app/src", recursive=True)
            await fs.write("/my-app/src/greet.py", b"""\
def greet(name: str) -> str:
    return f"Hello, {name}!"
""")

            # Run tests
            bash = fs.bash(cwd="/my-app", env={"PYTHONPATH": "/my-app"})
            result = await bash.exec("python -c 'from src.greet import greet; print(greet(\"World\"))'")
            print(result.stdout.decode())

            if result.exit_code == 0:
                print(f"Done. Change ID: {change_id}")

asyncio.run(main())

API Reference

MesaFileSystem

MethodSignatureReturns
read(path)bytes
write(path, content)None
append(path, content)None
exists(path)bool
stat(path)FsStat
lstat(path)FsStat
readdir(path)list[str]
realpath(path)str
readlink(path)str
resolve_path(base, path)str
mkdir(path, *, recursive=)None
rm(path, *, recursive=, force=)None
cp(src, dest, *, recursive=)None
mv(src, dest)None
chmod(path, mode)None
symlink(target, link)None
utimes(path, atime_ms, mtime_ms)None
bash(*, env=, cwd=, timeout_ms=)Bash
All methods except resolve_path and bash are async.

Bash

MethodSignatureReturns
exec(commands)ExecResult
exec is async.

Config Types

TypeConstructor
RepoConfig(name, *, bookmark=None, change_id=None)
DiskCacheConfig(path, *, max_size_bytes=None)