# Create bookmark
Source: https://docs.mesa.dev/api-reference/bookmark/create-bookmark
/openapi.json post /{org}/{repo}/bookmarks
Create a new bookmark from an existing change id
**Required scope:** `write`
# Delete bookmark
Source: https://docs.mesa.dev/api-reference/bookmark/delete-bookmark
/openapi.json delete /{org}/{repo}/bookmarks/{bookmark}
Delete a bookmark from a repository
**Required scope:** `write`
# Get bookmark
Source: https://docs.mesa.dev/api-reference/bookmark/get-bookmark
/openapi.json get /{org}/{repo}/bookmarks/{bookmark}
Get a single bookmark by name
**Required scope:** `read`
# List bookmarks
Source: https://docs.mesa.dev/api-reference/bookmark/list-bookmarks
/openapi.json get /{org}/{repo}/bookmarks
List bookmarks in a repository, optionally filtered by a bookmark-name glob
**Required scope:** `read`
# Merge bookmarks
Source: https://docs.mesa.dev/api-reference/bookmark/merge-bookmarks
/openapi.json post /{org}/{repo}/bookmarks/merge
Merge the source bookmark into the target bookmark. The target bookmark is advanced to the merge result.
**Required scope:** `write`
# Move bookmark
Source: https://docs.mesa.dev/api-reference/bookmark/move-bookmark
/openapi.json patch /{org}/{repo}/bookmarks/{bookmark}
Move an existing bookmark forward to a different change. Backward or sideways moves require allow_backwards.
**Required scope:** `write`
# Create change
Source: https://docs.mesa.dev/api-reference/change/create-change
/openapi.json post /{org}/{repo}/changes
Create a new change on top of an existing base change, optionally applying initial file operations. File content must be base64-encoded.
**Required scope:** `write`
# Get change
Source: https://docs.mesa.dev/api-reference/change/get-change
/openapi.json get /{org}/{repo}/changes/{change_id}
Retrieve a specific change by its JJ change id
**Required scope:** `read`
# Get diff
Source: https://docs.mesa.dev/api-reference/change/get-diff
/openapi.json get /{org}/{repo}/diff
Retrieve the diff between two change ids. In the JSON response, conflicted paths are excluded from `entries[]` and surface only in `conflicted_entries[]` (with per-side target/base/source content); the two arrays are mutually exclusive. When the client requests `Accept: text/plain`, the response is a full unified patch that includes every changed path, with conflicted paths rendered inline using JJ-style `<<<<<<<` / `>>>>>>>` markers.
**Required scope:** `read`
# List changes
Source: https://docs.mesa.dev/api-reference/change/list-changes
/openapi.json get /{org}/{repo}/changes
List current reachable changes for a repository bookmark
**Required scope:** `write`
# Patch change
Source: https://docs.mesa.dev/api-reference/change/patch-change
/openapi.json patch /{org}/{repo}/changes/{change_id}
Patch an existing change by updating metadata and/or applying file operations, then snapshot the result into a new change-owned commit. File content must be base64-encoded. When the change is conflicted, `files` writes are refused; submit a `resolutions` array instead to incrementally resolve conflicts — when every contested path is resolved the change becomes clean. `files` and `resolutions` are mutually exclusive on a single PATCH. A PATCH on a conflicted change without `resolutions` returns 409 MERGE_CONFLICT with a `details.conflict_paths` array listing the still-conflicted paths.
**Required scope:** `write`
# Get content
Source: https://docs.mesa.dev/api-reference/content/get-content
/openapi.json get /{org}/{repo}/content
Get file content or directory listing at a path. Use Accept: application/json for the JSON union response, or Accept: application/octet-stream for raw file bytes. Directory + octet-stream requests return 406 Not Acceptable.
File, symlink, and directory responses include an optional `metadata` field with the path's metadata when any is set; the field is omitted otherwise. Values are plain UTF-8 strings. Directory listings additionally surface per-entry metadata inline on each row in `entries[]` (also UTF-8 strings, omitted when empty). Reads at a historical `change_id` return today's metadata for the resolved path, not the values as of that change (metadata is stored separately from Git history).
Directory listings can be filtered by metadata predicates: `?metadata[key]=value`, repeatable. Multiple keys are AND-ed (exact-value match). Entries without metadata (or with mismatched values) are dropped from `entries[]`. The filter is ignored when the resolved path is a file or symlink.
**Required scope:** `read`
# whoami
Source: https://docs.mesa.dev/api-reference/org/get-caller-identity
/openapi.json get /whoami
Get the authenticated organization, effective scopes, and public-key metadata
**Required scope:** `read`
# Get organization
Source: https://docs.mesa.dev/api-reference/org/get-organization
/openapi.json get /{org}
Get organization metadata and repository counts
**Required scope:** `read`
# Bulk update tags
Source: https://docs.mesa.dev/api-reference/repo/bulk-update-tags
/openapi.json patch /{org}/repos/tags
Bulk set or remove a tag across repositories. This operation is idempotent.
**Required scope:** `write`
# Create repository
Source: https://docs.mesa.dev/api-reference/repo/create-repository
/openapi.json post /{org}/repos
Create a new repository in the organization
**Required scope:** `write`
# Delete repository
Source: https://docs.mesa.dev/api-reference/repo/delete-repository
/openapi.json delete /{org}/{repo}
Permanently delete a repository and all its data
**Required scope:** `write`
# Get repository
Source: https://docs.mesa.dev/api-reference/repo/get-repository
/openapi.json get /{org}/{repo}
Get metadata for a specific repository
**Required scope:** `read`
# Get upstream sync
Source: https://docs.mesa.dev/api-reference/repo/get-upstream-sync
/openapi.json get /{org}/{repo}/upstream/syncs/{syncId}
Get one sync for the repository upstream.
**Required scope:** `read`
# List repositories
Source: https://docs.mesa.dev/api-reference/repo/list-repositories
/openapi.json get /{org}/repos
List repositories in the organization using cursor pagination
**Required scope:** `read`
# List upstream syncs
Source: https://docs.mesa.dev/api-reference/repo/list-upstream-syncs
/openapi.json get /{org}/{repo}/upstream/syncs
List syncs for the repository upstream, newest first.
**Required scope:** `read`
# Sync upstream
Source: https://docs.mesa.dev/api-reference/repo/sync-upstream
/openapi.json post /{org}/{repo}/upstream/syncs
Enqueue a sync for the repository upstream. Returns the sync, which the worker processes asynchronously. Read `repo.upstream.latest_sync`, fetch `GET /:repo/upstream/syncs/:syncId`, or list `GET /:repo/upstream/syncs` for status.
**Required scope:** `write`
# Update repository
Source: https://docs.mesa.dev/api-reference/repo/update-repository
/openapi.json patch /{org}/{repo}
Update repository name, default bookmark, or tags. Tags are patched: omitted keys are unchanged, string values add or update a tag, and null values remove a tag.
**Required scope:** `write`
# Create webhook target
Source: https://docs.mesa.dev/api-reference/webhook-target/create-webhook-target
/openapi.json post /{org}/webhook-targets
Create a webhook target for the organization
**Required scope:** `admin`
# Delete webhook target
Source: https://docs.mesa.dev/api-reference/webhook-target/delete-webhook-target
/openapi.json delete /{org}/webhook-targets/{webhookTargetId}
Delete a webhook target from the organization
**Required scope:** `admin`
# Get webhook target
Source: https://docs.mesa.dev/api-reference/webhook-target/get-webhook-target
/openapi.json get /{org}/webhook-targets/{webhookTargetId}
Get a webhook target by ID
**Required scope:** `admin`
# List webhook targets
Source: https://docs.mesa.dev/api-reference/webhook-target/list-webhook-targets
/openapi.json get /{org}/webhook-targets
List webhook targets configured for the organization
**Required scope:** `admin`
# Update webhook target
Source: https://docs.mesa.dev/api-reference/webhook-target/update-webhook-target
/openapi.json patch /{org}/webhook-targets/{webhookTargetId}
Update a webhook target for the organization. Any provided field replaces its current value; omitted fields are left unchanged. Pass `repo_ids: null` to clear the repo filter and make the target org-wide.
**Required scope:** `admin`
# Overview
Source: https://docs.mesa.dev/content/api-reference/overview
Get started with the Mesa HTTP API.
The Mesa HTTP API provides direct programmatic access to repository management, file operations, and version control. Use one of our SDKs when you want a client library, or call the HTTP API directly when you want full control over requests and transport behavior.
Looking for client libraries instead of raw HTTP requests? Start with the [TypeScript SDK reference](/content/reference/ts/index).
## Base URL
All API endpoints are available at:
```
https://api.mesa.dev/v1
```
## Authentication
Every API request carries a bearer credential in the `Authorization` header. For new integrations, use a short-lived access token:
```bash theme={null}
curl -H "Authorization: Bearer $MESA_ACCESS_TOKEN" \
https://api.mesa.dev/v1/{org}/repo
```
See [Authentication](/content/concepts/authentication) for private keys and access-token minting.
## Request Format
* All request bodies should be JSON with `Content-Type: application/json`
* Path parameters use the format `/{org}/{repo}/...` where `org` is your organization slug
* Query parameters are used for filtering, pagination, and optional settings
## Response Format
Most endpoints return JSON responses. However, some endpoints support content negotiation via the `Accept` header to return alternative formats:
| Endpoint | Default | Alternative |
| --------------------------- | ------------------------------------ | -------------------------------------- |
| `GET /{org}/{repo}/content` | `application/json` (base64 encoded) | `application/octet-stream` (raw bytes) |
| `GET /{org}/{repo}/diff` | `application/json` (structured diff) | `text/plain` (raw unified diff) |
Example JSON response:
```json theme={null}
{
"id": "repo_123",
"name": "my-repo",
"default_bookmark": "main"
}
```
Example requesting raw file content:
```bash theme={null}
curl -H "Authorization: Bearer $MESA_ACCESS_TOKEN" \
-H "Accept: application/octet-stream" \
https://api.mesa.dev/v1/{org}/{repo}/content?path=README.md
```
## Pagination
List endpoints support cursor-based pagination:
| Parameter | Type | Description |
| --------- | ------ | ----------------------------------------------- |
| `limit` | number | Maximum items to return (default: 50, max: 100) |
| `cursor` | string | Cursor from previous response for next page |
Paginated responses include:
```json theme={null}
{
"items": [...],
"next_cursor": "abc123",
"has_more": true
}
```
## Error Handling
Errors return a consistent JSON structure with an appropriate HTTP status code:
```json theme={null}
{
"error": {
"code": "REPO_NOT_FOUND",
"message": "Repository 'my-repo' not found"
}
}
```
### Error Codes
| Code | HTTP Status | Description |
| -------------------- | ----------- | ----------------------------------------------- |
| `UNAUTHORIZED` | 401 | Invalid or missing access token |
| `FORBIDDEN` | 403 | Valid access token but insufficient permissions |
| `REPO_NOT_FOUND` | 404 | Repository does not exist |
| `BOOKMARK_NOT_FOUND` | 404 | Bookmark (branch) does not exist |
| `COMMIT_NOT_FOUND` | 404 | Commit SHA not found |
| `FILE_NOT_FOUND` | 404 | File path not found |
| `REPO_EXISTS` | 409 | Repository already exists |
| `BOOKMARK_EXISTS` | 409 | Bookmark (branch) already exists |
| `MERGE_CONFLICT` | 409 | Merge cannot be completed due to conflicts |
| `INVALID_REQUEST` | 400 | Malformed request body or parameters |
Server errors (5xx) additionally include an `error.trace_id`. Quote it when reporting an issue.
## Scopes
Access tokens use scoped permissions:
| Scope | Description |
| ------- | ---------------------------------------------------------------------------------- |
| `read` | View repositories, changes, bookmarks, and content |
| `write` | Modify repositories, changes, bookmarks, and content |
| `admin` | Everything `write` can do, plus webhook management and other privileged operations |
Scope hierarchy: **`admin`** → **`write`** → **`read`**. Each scope includes everything below it.
# Mesa CLI Changelog
Source: https://docs.mesa.dev/content/changelog/mesa-cli
Release history for the Mesa CLI.
All notable changes to this project will be documented in this file.
The format is based on [Common Changelog](https://common-changelog.org/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 0.48.0 - 2026-09-10
### Changed
* **Breaking:** `mesa mount` now requires a `--layout ` flag
* **Breaking:** Require `install.sh` callers to specify a CLI version or explicitly request `--version latest`
* Show every ordered commit author in `mesa log` and `mesa show`, and show `No author found` for the virtual root
### Removed
* **Breaking:** Remove `MESA_REPO_REVS` and the hidden `--repo-revs` CLI flag; specify checkout revisions with `at` in the mount layout instead
* **Breaking:** Remove `mesa org refresh`; a mount's repositories are fixed by its layout and changing them requires remounting
* **Breaking:** Remove `mesa open`, `mesa repo open`, and `mesa repo close`. Layout must specified at mount time via the `--layout` flag.
* **Breaking:** Remove `org_count` from `mesa stats --json`; `repo_count` remains
* **Breaking:** Remove the no-op `--all` flag from `mesa repo list`. The list command already shows all accessible repos by default
* **Breaking:** Stop reading `config.toml`. Configure the CLI with environment variables and `MESA_ACCESS_TOKEN`. `--config-path`/`-c`, `--no-config-file`, `MESA_NO_CONFIG_FILE`, `mesa doctor`, `mesa dump-config-template`, and `-y`/`--non-interactive` go with it
* **Breaking:** Remove MacOS mount support from the Mesa CLI
* **Breaking:** Make `MESA_ACCESS_TOKEN` the CLI's only credential and organization source. Remove raw API-key configuration, `MESA_API_KEY`, `MESA_ORG`, `MESA_ORGS`, `--org` flags, and config-file credential or organization fallbacks
## \[0.47.3] - 2026-08-26
### Fixed
* Write daemonized mount logs to `/tmp/mesa-/mesa.log`, fall back to stdout
## \[0.47.2] - 2026-08-21
### Fixed
* Fix MesaFS transport stalls after snapshot resume by detecting dead transports, retrying replay-safe REST requests on replacement live connections, and failing every MesaFS REST attempt over 10 seconds
* Fix native crash reporting replacing the original termination signal with `SIGSEGV`
## \[0.47.1] - 2026-08-16
### Fixed
* Fix active writes returning `ESTALE` when a background WAL flush refreshes repository paths
* Ensure that all CLI commands derive the organization from `MESA_ACCESS_TOKEN` so that `MESA_ORG` is no longer required
* Fix slow `mount()` initialization for layouts that specify a checked-out revision with `at`
## \[0.47.0] - 2026-08-14
### Fixed
* Fix reads, writes, truncation, mode changes, and mtime updates through file descriptors after unlink or rename replacement without recreating the removed path or changing its replacement, while checkout-stale mutations remain rejected with `ESTALE`
## \[0.46.0] - 2026-08-13
### Changed
* **Breaking:** `layout.json` files no longer accept repos with a top-level `bookmark` / `changeId` field. To specify a checkout revision, use `at`.
* Allow CLI mounts to run with only `MESA_ACCESS_TOKEN`
### Added
* Add `branchedFrom` to layout entries to fork a new change the first time a repo is opened
### Removed
* **Breaking:** Remove `--vcs-url`, `MESA_VCS_URL`, and `debug.__service-vcs-url` configuration options.
## \[0.45.0] - 2026-08-11
### Added
* Add `mesa open --repo --mode --path` with `--at` or `--from`/`--as`/`--describe` to open a repository checkout (create change → optional `--as` bookmark → publish checkout; omit `--as` for an anonymous tip; `--repo` is auto-detected from CWD when omitted; custom `--path` FUSE presentation lands with mount layouts)
## \[0.44.1] - 2026-08-09
### Changed
* Split filesystem write batches to the server-advertised VCS operation and message-size limits
## \[0.44.0] - 2026-08-07
### Added
* Add support for custom filesystem layouts with `mesa mount --layout `: the layout defines the complete namespace, mounting the selected repositories at the paths it declares with per-repository `ro`/`rw` modes. Repository names resolve within the organization `MESA_ORG` selects, or the sole configured organization when `MESA_ORG` is unset. Without a layout, `mesa mount` exposes the organization browse tree as before
* Add `mesa checkpoint [-m ]` to flush pending writes (serializing concurrent writes and tree mutations in the same repo), optionally describe the current change, create a new descendant change, and advance bookmarks onto that descendant (requires the checkout to already be on a bookmark). Concurrent bookmark-tip races raise a retryable conflict (distinct from not-on-a-bookmark and from a half-applied checkout failure). Omit `-m` to preserve the existing description; pass `-m ''` to clear it; any other `-m` value overwrites. Prints the now-active change id on the first line (same as `mesa new` / `mesa edit`) and `saved ` for the described change on the second line
### Changed
* `mesa root` and repository-aware commands now work from custom and nested layout paths
* Change `mesa bookmark create` so recreating an existing bookmark at the same commit succeeds (exit 0); creating the same name at a different commit still fails with `already exists`. Success output is now `Bookmark '' is at the requested revision.` for both create and same-target recreate
### Fixed
* Fix writes through unlinked file descriptors recreating deleted files or overwriting replacements
* Fix fresh pathname reads and metadata remaining stale while a descriptor from before a checkout or realtime update stays open
## \[0.43.0] - 2026-08-02
### Changed
* **Breaking:** Expand `mesa bookmark move` to select multiple bookmarks by name, glob, or repeated `--from`; make `-t/--to` canonical with `-r/--revision` aliases; and require `-B/--allow-backwards` for backward or sideways moves
## \[0.42.0] - 2026-07-16
### Changed
* Stop checking for newer CLI releases automatically when commands run; use `mesa version --check` to check explicitly so intentionally pinned installations are not prompted to upgrade
### Fixed
* Fix realtime mount updates after a local write so subsequent remote creates, deletes, and renames appear without remounting
## \[0.40.0] - 2026-07-12
### Changed
* mesa on macOS no longer requires macFUSE — installing or upgrading via Homebrew now sets up everything automatically, with no kernel extension, security approval, or reboot. If you previously installed macFUSE for mesa, you can uninstall it
## \[0.39.0] - 2026-07-08
### Added
* Add the `mesa diff` command.
* Add `MESA_CACHE_BLOCK_SIZE` / `cache.block-size` to tune the MesaFS disk-cache block size and reduce file-descriptor pressure for large disk caches.
### Changed
* `mesa log` now shows your whole change tree instead of just the current checkout's ancestry, so a change you started but never pointed a bookmark at no longer disappears from the log. Pass `-r ` to scope the log to a single revision's history as before.
* Distribute mesa on macOS as a Homebrew cask instead of a formula; `install.sh` migrates existing formula installs
### Fixed
* Cap auto-sized MesaFS disk caches by the process open-file limit so large free disks do not exhaust file descriptors during mount startup.
## \[0.38.0] - 2026-06-22
### Changed
* Pass the short change ID shown in `mesa log` — or any unique prefix — to any command that takes a revision (`mesa edit`, `mesa new`, `mesa show`, `mesa log`, `mesa describe`, `mesa bookmark`). `mesa log` now prints each change at its shortest unambiguous length, so the ID you see is always one you can type.
* `mesa show` now leads with the change ID — the handle you act on — and moves the commit hash to a secondary line.
### Removed
* No longer expose the synthetic `user.mesa.daemon-pid` xattr
### Fixed
* Fix newly created files transiently disappearing from the mount during bursts of writes
## \[0.37.0] - 2026-06-17
### Added
* Install a specific CLI version on Linux with `curl -fsSL https://mesa.dev/install.sh | sh -s -- --version `. The version is pinned through your package manager (apt/apk/dnf) using the existing repository.
* Add environment variables for every remaining config file option: `MESA_DAEMON_LOG_FILE`, `MESA_DAEMON_LOG_COLOR`, `MESA_PREFETCH_ENABLED`, `MESA_PREFETCH_MAX_DEPTH`, `MESA_PREFETCH_MAX_CONCURRENCY`, `MESA_PREFETCH_CACHE_PRESSURE_LIMIT`, and `MESA_MESAIGNORE_PATH` (explicit global `.mesaignore` location)
* Add `MESA_REPO_REVS` to pin per-repo revisions from the environment, e.g. `MESA_REPO_REVS="acme/web=bookmark:main,acme/api=change-id:abc123"`
* Add `MESA_NO_CONFIG_FILE` to skip reading any (deprecated) config file, for sandboxes and other hermetic environments
### Changed
* When you mount a repo, writes now alter the revision you mounted at. Previously, the first write on a mount would create a new change *on top* of your specified revision. To emulate the old behavior, use `mesa new` after mounting.
* Mount every repo the API key can access, always; `mount = "explicit"` in a config file is now ignored with a deprecation warning, and access is restricted by scoping the API key. Per-repo overrides (rev pins, read-only) still apply
* Deprecate `mesa repo open` and `mesa repo close`; both are now hidden no-ops that warn and exit 0, since there is no explicit mount list to edit
* Change `mesa repo list` to list repos from the API instead of the local mount configuration; `--all` is a hidden deprecated no-op
* Deprecate TOML config files (`config.toml` and `credentials.toml`): existing files are still read with environment variables taking precedence, but `mesa` now logs a warning naming the environment variable replacement for every setting the file contains. A future release will stop reading config files entirely — that removal will be the breaking change
* Deprecate `-y`/`--non-interactive`; the flag still parses but has no effect now that config files are never created
* Deprecate `mesa doctor` and `mesa dump-config-template`; both still work but only exist to support deprecated config files
* Change the daemon to exit at startup with a clear error when no organizations are configured, instead of mounting an empty filesystem
### Removed
* Remove the interactive onboarding wizard; `mesa` no longer creates config files on first run — configure it with `MESA_ORG` and `MESA_API_KEY` instead
### Fixed
* Make `MESA_API_KEY` take precedence over `credentials.toml`. Previously a per-org `[organizations.].api-key` entry in `credentials.toml` outranked `MESA_API_KEY`, so a stale file key silently shadowed the environment key and produced confusing `401` errors. Credentials now resolve as `MESA_ORGS` → `MESA_API_KEY` → per-org `credentials.toml` → top-level `credentials.toml`
## \[0.36.0] - 2026-06-09
### Added
* Support installing Mesa on RPM-based Linux distributions with `dnf`, including Amazon Linux 2023 environments such as Vercel Sandbox.
* Add the `MESA_ORG` (organization to configure) and `MESA_API_KEY` (credential for this process) environment variables. `MESA_API_KEY` accepts an API key or an access token, so a short-lived token minted outside a sandbox can be injected without the raw API key ever entering it.
### Changed
* The mesa daemon now runs mounts on a self-expiring access token signed locally from your API key, instead of mounting with the raw key. The mount lasts up to 24 hours and then expires.
### Removed
* Remove the MesaFS secret store, the `mesa auth` commands, and legacy secret backend configuration. Mesa CLI now reads API keys only from `MESA_API_KEY` or a plaintext `credentials.toml` next to `config.toml`.
## \[0.34.0] - 2026-06-01
### Added
* Enable realtime updates by default for MesaFS mounts. Edits from other clients checked out on the same change now appear automatically. If simulatenous edits conflict, both edits will show up in the file, delimited by conflict markers.
### Changed
* MesaFS FUSE `flush(2)` no longer blocks waiting for confirmation that writes were received by the Mesa backend
* Update global `.mesaignore` handling: the CLI now loads `.mesaignore` from the same directory as the active `config.toml`; if no file exists, Mesa uses its built-in default ignore rules. A custom `.mesaignore` fully replaces those defaults, so an empty file disables global ignore rules for that mount. SDK and other non-CLI mounts no longer inherit ignore rules from the CLI config directory.
### Removed
* Remove the experimental CRDT-backed multiplayer mode and related real-time filesystem event handling. Realtime is now enabled by default in MesaFS.
## \[0.33.0] - 2026-05-26
### Added
* Add per-repo `read-only` to `[organizations..repos.]` in `config.toml`. Set `read-only = true` and the daemon rejects writes to that repo with `EROFS`, while other repos in the same mount stay writable.
* Mount and resolve conflicted changes in the filesystem. Checking out a conflicted change (e.g. after a conflicted merge) now succeeds: conflicted files read back with JJ-style conflict markers (`<<<<<<<` … `>>>>>>>`), `stat()` reports the materialized marker size, and writing marker-free content back resolves the conflict and persists across remounts. Previously `mesa edit` on a conflicted bookmark failed with "commit has no tree OID" because the server stored the structural tree separately from the commit row; the server now records it on the commit directly so the daemon mounts the conflicted commit through the normal tree-fetch path.
## \[0.32.0] - 2026-05-22
### Added
* Real-time kernel filesystem events in multiplayer mode. When peers modify files in a shared session, the FUSE layer now delivers inotify/FSEvents notifications so editors and build tools pick up changes without polling.
## \[0.30.0] - 2026-05-19
### Added
* Add `mesa new -m, --message ` for setting the description of a newly created change. Omitting the option creates the change with no description.
## \[0.29.2] - 2026-05-17
No user-facing changes.
## \[0.29.0] - 2026-05-15
### Added
* Support POSIX extended attributes on any path in a mounted Mesa repo. `setxattr` / `getxattr` / `listxattr` / `removexattr` (and their `l*xattr` symlink variants) round-trip values under the **`user.mesa.*`** namespace; use `setfattr` / `getfattr` on Linux or `xattr` on macOS. Names outside `user.mesa.*` (including the broader `user.*`, plus `security.*`, `system.*`, `trusted.*`) are short-circuited at the FUSE layer — set rejects with `EPERM`, get/remove answer `ENODATA` instantly with no VCS round-trip, so kernel-issued queries (e.g. `getxattr(security.capability)` on every `exec(2)`) don't penalize file-heavy workloads like `npm install`. Per-attribute values capped at 64 KiB and names at 255 bytes; per-path total is storage-bounded (\~64-72 KiB raw, base64-inflated under the hood). Synthetic xattrs (`user.mesa.org`, `user.mesa.repo`, `user.mesa.daemon-pid`) win over user-set values with the same name on read
### Fixed
* Empty repos are now usable without workarounds — `mesa edit`, bookmark create, write,
and remount all work on freshly created repos where `main` points at ZERO\_OID
([MES-1387](https://linear.app/mesa-dev/issue/MES-1387))
### Changed
* The `Rev` RPC type gains an `EmptyRoot` unit variant for empty-repo bootstrap.
**Restart the mesa daemon after upgrading** — a new CLI connecting to an old
daemon will fail to deserialize the new variant.
## \[0.28.1] - 2026-05-06
### Added
* Add `mesa show` subcommand for inspecting a revision's commit metadata and patch. Supports `--name-only`, `--summary`, `--stat`, and `--no-patch` flags with colorized output.
### Changed
* `mesa log` now shows change history instead of commit history, matching the mental model of changes as the primary unit of work.
* Coordinate cache budgets across `mesa` instances in the same process;
unset `cache.max_size` and `cache.max_memory_size` in `config.toml` now auto-size against system resources instead of being unbounded
### Fixed
* Fix `rg` and other tools that resolve `/..` in just-bash mode by clamping `resolve_path` at the filesystem root.
## \[0.28.0] - 2026-05-03
### Added
* Added support for `.mesaignore` files which exactly follow `.gitignore` semantics.
* Added support for a global `.mesaignore` file as a sibling to the `config.toml` file. `mesa`
creates this file for you during first launch. Existing users can use
`mesa dump-default-mesaignore`.
* Added `mesa dump-default-mesaignore` to print out the default `.mesaignore` installed during
onboarding.
* Added support for conflicts in `mesa`'s filesystem. Conflicting files render `jj`-style conflict
semantics.
### Fixed
* Fixed race condition in cache deallocation theoretically causing crashes.
## \[0.27.0] - 2026-04-30
No user-facing changes.
## \[0.26.0] - 2026-04-29
No user-facing changes.
# Python SDK Changelog
Source: https://docs.mesa.dev/content/changelog/python-sdk
Release history for the Python SDK.
All notable changes to this project will be documented in this file.
The format is based on [Common Changelog](https://common-changelog.org/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 0.48.0 - 2026-09-10
### Changed
* **Breaking:** Rename `SigningKeyAuthor` to `Author` for commit attribution and rename the existing committer `Author` dataclass to `Committer`
* **Breaking:** Consolidate `LayoutSpec` and the `Layout` wrapper into a validated, plain `Layout` mapping, serialize it with `json.dumps()`, and rename `FsNamespace` to `FilesystemDefinitions`
### Added
* Add `authors` and `authored_at` to `change.created` and `change.evolved` webhook events; `email` is `null` when none was supplied
* Add `authors` and `authored_at` to change results
### Removed
* **Breaking:** Remove the deprecated singular `author` from change results and from normalized `change.created` / `change.evolved` webhook events; read `authors` and `authored_at` instead. Raw `mesa_rest` responses keep `author` for API compatibility
* **Breaking:** Remove the undocumented `block_size_bytes` keyword from `DiskCacheConfig`. Disk cache block size is an internal tuning knob and was never declared in the type stubs
* **Breaking:** Remove `MesaFileSystem.connect()` from the public surface; open a filesystem with `mesa.fs(layout=..., authors=...).mount()`, which mints the mount's token with scopes derived from the layout. `MesaFileSystem` remains exported for annotating the value `mount()` yields
* **Breaking:** Remove legacy comma-separated repository tag filters from `mesa.repos.list()`; pass a structured filter dictionary instead
* **Breaking:** Remove `auth` and access-token construction from `Mesa`; construct with `Mesa()` or `Mesa(private_key=...)` plus the usual keyword options, and pass scoped tokens to the CLI, mounted MesaFS environments, or direct REST calls instead
* **Breaking:** Remove API-key management methods
* **Breaking:** Remove `mesa.tokens.create()`. Mint through a layout definition instead: `mesa.fs(layout=..., ttl=...).token()`, which scopes the token to the repositories the layout declares
## \[0.47.1] - 2026-08-16
### Fixed
* Fix slow `mount()` initialization for layouts that specify a checked-out revision with `at`
## \[0.47.0] - 2026-08-14
### Changed
* **Breaking:** Change the app-mount Bash default working directory from `/home/user` to `/`
## \[0.46.0] - 2026-08-13
### Changed
* **Breaking:** Replace the `mesa.fs.mount(repos=...)` mount syntax with the new FS definition syntax: `mesa.fs(layout=...).mount(...)`
* **Breaking:** Change `repo()` to select a checkout revision with the `at` field instead of `bookmark` / `change_id`
### Added
* Add `branched_from` to `repo()` to fork a new change the first time the mount opens the repo
### Removed
* **Breaking:** Removed the async `mesa.resolve_org()` method. The organization slug is now available synchronously via `mesa.org.slug`.
* **Breaking:** Remove `org` from the Python SDK constructor and resource methods; the SDK derives the organization from the credential
* **Breaking:** Remove `vcs_url` option from the client configuration
* **Breaking:** Replace all singular `author` options with a plural `authors`
### Fixed
* Fix `repo()` `at` pins being overridden when the organization's configuration forks the repo on open
* Fix layout `repo()` silently dropping unrecognized keys in `at`, `branched_from`. The SDK now raises instead of pinning the wrong revision
* Fix layout `repo()` silently accepting an invalid fully-qualified `"org/repo"` name; a repo name containing `/` now raises
## \[0.45.0] - 2026-08-11
### Changed
* Point package source and issue links to the public SDK repository
* **Breaking:** Remove top-level `RepoConfig.bookmark`, `RepoConfig.change_id`, and `RepoConfig.read_only`. Use `at` and `mode` instead
* **Breaking:** `RepoConfig.mode` is now a `MountMode` enum (`MountMode.rw` / `MountMode.ro`); the strings `"rw"` / `"ro"` remain accepted. The public `MountMode` name is this enum (layout JSON still uses `"ro"` / `"rw"` string literals)
* **Breaking:** Resolve `fs.changes.new(..., bookmark=...)` (and `edit` / mount `at={"bookmark": ...}`) strictly against `refs/heads/`. A string that is not a bookmark no longer falls through server-side revspec resolution to a change id; pass `change_id` to fork from a change
### Added
* Add mount `RepoConfig` fields `mode`, `at`, and `branched_from` to open an existing revision or fork a new revision at mount time (create change → optional `as.bookmark` → publish checkout; omit `as.bookmark` for an anonymous tip)
### Removed
* **Breaking:** Remove API-key authentication from the SDK, including `api_key`, `MESA_API_KEY`, legacy HS256 token minting, and `MissingApiKeyError`; use `private_key` or `auth["access_token"]` instead
## \[0.44.1] - 2026-08-09
### Changed
* Split native filesystem write batches to the server-advertised VCS operation and message-size limits
## \[0.44.0] - 2026-08-07
### Changed
* Resolve the organization for `auth["access_token"]` locally from its JWT issuer and reject opaque or malformed tokens instead of making an implicit `/whoami` request
### Added
* Add custom mount layouts, created by calling `mesa.fs(layout=..., ttl=...)`
* Add `token()` and `layout()` on the FS mount definition for serializing a token and layout file that can be passed into a sandbox
* Add `await fs.changes.checkpoint(repo, message=None)` to flush pending writes (serializing concurrent writes and tree mutations in the same repo), optionally describe the current change, create a new descendant change, and advance bookmarks onto that descendant (requires the checkout to already be on a bookmark). Concurrent bookmark-tip races raise a retryable conflict (distinct from not-on-a-bookmark and from a half-applied checkout failure). Omit `message` to preserve the existing description; pass `message=""` to clear it; any other string overwrites. Returns `CheckpointResult` with `saved_change_oid` (saved change) and `active_change_oid` (now-active empty descendant)
* Add private-key authentication and local Ed25519 token signing. Token minting, commit-producing REST operations, and MesaFS mounts require operation-level ordered authors; non-authoring REST credentials remain authorless
## \[0.43.0] - 2026-08-02
### Changed
* **Breaking:** Require REST bookmark moves to advance history unless `allow_backwards` is set
* **Breaking:** Require native filesystem bookmark moves to advance history by default and add an `allow_backwards` override
### Added
* Add repository ID restrictions to locally signed access tokens with `mesa.tokens.create(repo_ids=[...])`
## \[0.39.0] - 2026-07-08
### Added
* Add structured tag filter dictionaries to `mesa.repos.list()`, with `$`-prefixed case-insensitive operators (`$and`, `$or`, `$not`, `$eq`, `$in`, `$contains`, `$starts_with`, `$ends_with`, `$exists`) and case-insensitive tag matching
### Deprecated
* Deprecate legacy comma-separated repo tag filters in favor of structured tag filter dictionaries
## \[0.38.0] - 2026-06-22
### Fixed
* Fix files written through the SDK transiently disappearing during bursts of writes
## \[0.37.0] - 2026-06-17
### Fixed
* Repeated filesystem operations on the same repository are now dramatically faster
## \[0.36.0] - 2026-06-09
### Added
* Add `mesa.tokens.create(...)` to sign a self-expiring access token (JWT) locally from an API key, scoped to a subset of the key's scopes and to repositories by full `org/repo` name. Signing is local with no network call; a `ttl_seconds` outside 1 second to 24 hours raises `InvalidOptionsError`
* Add a `ttl` option to `mesa.fs.mount()` to set how long the mount lasts, in seconds (default 3600, max 86400). The mount signs one access token for that lifetime and expires with it
* Add `MissingCredentialError` as the preferred name for the error raised when no API key is provided (directly or via `MESA_API_KEY`)
### Changed
* `mesa.fs.mount()` now signs a short-lived access token locally from your API key instead of creating an ephemeral API key on the server. Repos are scoped by full `org/repo` name so signing needs no network round-trip, no server-side credential is created or left behind, and the mount expires on its own when the token does
### Deprecated
* Deprecate `MissingApiKeyError` in favor of `MissingCredentialError`. The old name is retained as an alias of the same class, so `except MissingApiKeyError` keeps working
## \[0.34.0] - 2026-06-01
### Added
* Changes in Mesa are now realtime by default. Concurrent editors mounted on the same change will see one another's modifications within a few seconds.
* Add `MesaFileSystem.subscribe(handler)` for filesystem invalidation callbacks across mounted changes. Call `unsubscribe()` on the returned subscription to stop.
### Removed
* Remove the experimental `multiplayer` filesystem APIs.
## \[0.33.0] - 2026-05-26
### Added
* Add `read_only` to `RepoConfig`. Pass `RepoConfig(name="my-repo", read_only=True)` to `fs.mount(...)` and the mesa daemon rejects writes to that repo with an `OSError` whose `errno` is `errno.EROFS`, so a single mount can carry a mix of writable and read-only repos.
* Add `type` aliases and `is_file()`, `is_dir()`, and `is_symlink()` helpers to generated content response models
### Changed
* Change responses now report parent change IDs instead of commit SHAs of prior versions of the same change.
### Removed
* **Breaking:** Remove the mount-wide `mode` parameter from `fs.mount(...)` and `MesaConfig`. Set `read_only` on each `RepoConfig` instead. The minted API key always carries `read` and `write` scopes; read-only enforcement is performed client-side by the mesa daemon
## \[0.32.0] - 2026-05-22
### Added
* Add `multiplayer.watch()` for async-iterator multiplayer event streaming. Returns a `MultiplayerEventStream` usable as an async context manager; yields `RemoteChangeEvent` (with `generation`) and `PeersChangedEvent` (with `count`).
* Add `multiplayer.on()` for callback-based multiplayer event streaming. Accepts a sync or async callback; returns an unsubscribe callable.
## \[0.31.0] - 2026-05-21
### Added
* Add `set_metadata` / `get_metadata` / `clear_metadata` on `MesaFileSystem` for per-file metadata as plain key/value pairs. `set_metadata` merges per key; passing `None` or an empty string as a value deletes that key, so one call can mix sets and deletes. `get_metadata` returns all keys including the read-only `org`/`repo`. `clear_metadata` removes every key on a path. Naming a reserved key raises `ValueError`.
### Changed
* **Breaking:** Rename the `mesa.content.get(...)` metadata surface from `xattrs` to `metadata`. Responses now expose optional `metadata` instead of `xattrs`, and directory listings filter via `metadata` key/value pairs instead of `xattr` pairs.
## \[0.30.0] - 2026-05-19
### Added
* Add `mesa.webhooks.on(...)` and `mesa.webhooks.receive(...)` for verifying and dispatching inbound Mesa webhooks
* Add `fs.bookmarks.move(repo, name, change_id=...)` for moving bookmarks from a mounted filesystem
* `mesa.content.get(...)` responses now expose optional `xattrs` metadata for files, symlinks, directories, and directory entries when present.
* Add `message` support to `fs.changes.new(...)` for setting the description of a newly created mounted-filesystem change
### Changed
* `mesa.changes.create(...)` and `mesa.changes.patch(...)` now accept empty string descriptions. Pass `None` to omit the field; pass `""` to create or update a change with no description.
* `mesa.bookmarks.merge(...)` now accepts `message`; pass `None` to use the generated merge description, or pass `""` to create the merge with no description.
## \[0.29.3] - 2026-05-18
### Fixed
* Make every `mesa.fs` write durable before the operation returns
## \[0.29.2] - 2026-05-17
### Added
* Add `mesa.bookmarks.get(repo=..., bookmark=...)` for direct bookmark lookup by name, and add `glob` filtering to `mesa.bookmarks.list(repo=..., glob=...)`
* Add optional `ref_globs` filters to `mesa.repos.sync_upstream(...)`: omit it to sync all supported branches and tags, or pass branch/tag glob strings such as `{"branches": "main"}`
### Fixed
* Fix `mesa.fs.mount()` dropping recently written file bytes by flushing pending mount writes before revoking the scoped API key
## \[0.29.0] - 2026-05-15
### Added
* Add `VIRTUAL_ROOT_CHANGE_ID` export for referencing the virtual-root sentinel change id from `mesa_sdk`
* Add `mesa.repos.sync_upstream(repo=..., direction=...)` to enqueue a sync with a repository's configured upstream. Returns the `sync` row; the worker processes it asynchronously. Read `repo.upstream.latest_sync`, call `mesa.repos.get_upstream_sync(repo=..., sync_id=...)`, list `mesa.repos.list_upstream_syncs(repo=...)`, or subscribe to `sync.{queued,in_progress,completed,failed}` webhooks
* Add `mesa.repos.get_upstream_sync(repo=..., sync_id=...)` and `mesa.repos.list_upstream_syncs(repo=...)` for reading repository upstream sync history
* `mesa.repos.create()` and `mesa.repos.update()` accept `upstream=UpstreamConfig(url=..., auth=...)` to set or replace the upstream remote inline. On `update()`, pass `upstream=None` to remove the upstream entirely, or omit the argument to leave it unchanged. Repository responses include the upstream configuration with `latest_sync`; secrets are never returned
* `UpstreamConfig.auth` is tri-state, mirroring the REST surface: default `UNSET` (on `update()` leaves any stored credential untouched; on `create()` produces a public upstream); `None` explicitly clears the stored credential; a `TokenAuth` or `UsernamePasswordAuth` sets or replaces it. Individual fields inside the auth object (`token`, `password`, `username`, `token_username`) are atomic and must be sent in full
* New public types in `mesa_sdk.types`: `UpstreamConfig`, `UsernamePasswordAuth`, `TokenAuth`, and the `UpstreamAuth` union
### Changed
* Resolving the default bookmark of a freshly created (never-written-to) repo continues to return the virtual root sentinel `change_id`. The server-side lazy-seed behavior is transparent to SDK consumers — the first write through any path advances the bookmark to a real change ([MES-1387](https://linear.app/mesa-dev/issue/MES-1387))
### Removed
* **Breaking:** Remove the public `mesa.raw` generated REST client attribute; use `mesa-rest` directly for low-level REST access
## \[0.28.2] - 2026-05-07
### Added
* Add `fs.changes.current(repo)` to query the currently active change on a mounted repo. Returns `ChangeInfo` with `change_id` and `commit_oid`.
## \[0.28.1] - 2026-05-06
### Changed
* Coordinate `MesaFileSystem` cache budgets across instances in a single process;
`disk_cache.max_size_bytes=None` now auto-sizes against system resources instead of being unbounded
### Fixed
* Fix segfault on process exit caused by crash handler outliving the Python interpreter. The native extension now uninstalls the crash handler via `atexit` before teardown.
* Fix `rg` and other tools that resolve `/..` by clamping `resolve_path` at the filesystem root.
## \[0.28.0] - 2026-05-03
### Added
* Added support for conflicts in the FS interfaces. Conflicting files render `jj`-style conflict
semantics.
### Fixed
* Raise `ConflictError` instead of a generic API error on concurrent bookmark writes.
* Fixed race condition in cache deallocation causing crashes on binding destruction.
## \[0.27.0] - 2026-04-30
### Changed
* Replace pure-Python wheel with platform-specific wheels containing a native
extension (manylinux, musllinux, macOS arm64). Python 3.10+ required.
* Filesystem errors use Python's built-in exception hierarchy
(`FileNotFoundError`, `IsADirectoryError`, `PermissionError`, etc.) instead of
`MesaError` subclasses.
### Added
* Add `mesa.fs.mount()` async context manager for mounting repos as a virtual
filesystem. Automatically mints a scoped API key on entry and revokes it on
exit (with a 1-hour server-side TTL fallback).
* Add `MesaFileSystem` python interface with full async file I/O: `read`,
`write`, `append`, `exists`, `stat`, `lstat`, `readdir`, `mkdir`, `rm`, `cp`,
`mv`, `chmod`, `symlink`, `readlink`, `realpath`, and `utimes`.
* Add `fs.changes` namespace for change management on a mounted filesystem:
`new`, `edit`, and `list`.
* Add `fs.bookmarks` namespace for bookmark management on a mounted filesystem:
`create` and `list`.
* Add `Bash` class for running shell commands against a mounted filesystem via
`fs.bash()`. Supports `env`, `cwd`, and `timeout_ms`.
## \[0.26.0] - 2026-04-29
### Fixed
* Fix repo list cache not being exercised through the SDK codepath, hitting an uncached API on every
call.
# REST API Changelog
Source: https://docs.mesa.dev/content/changelog/rest-api
Release history for the REST API.
All notable changes to this project will be documented in this file.
The format is based on [Common Changelog](https://common-changelog.org/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 0.48.0 - 2026-09-10
### Changed
* **Breaking:** Rename the unknown-public-key authentication error code from `JWT_UNKNOWN_SIGNING_KEY` to `JWT_UNKNOWN_PUBLIC_KEY`
* Report invalid API and VCS bearer authentication as an invalid access token instead of invalid credentials
* Deprecate the singular `author` field on change responses and `change.created` / `change.evolved` webhook payloads; read `authors` and `authored_at` instead
* Raise the per-file inline write limit for apply-ops and filesystem mounts from 20 MiB to 512 MiB, with a 512 MiB total per request
### Added
* Add `authors` and `authored_at` to change responses. `authors` lists every author in order, with `email` set to `null` when none was supplied; `authored_at` is the commit's author timestamp
* Add `authors` and `authored_at` to `change.created` and `change.evolved` webhook payloads, with the same shape as change responses
### Removed
* **Breaking:** Remove support for comma-separated repository tag filters from `GET /v1/:org/repos`; encode tag filters as structured JSON objects
* **Breaking:** Remove raw API-key authentication, API-key-derived HS256 access tokens, API-key management routes, and API-key metadata from `whoami`
### Fixed
* Fix intermittent `INTERNAL_ERROR` responses caused by stale database connections
* Avoid spurious ABORTED error when clients make racing writes on the same change
## \[0.46.0] - 2026-08-13
### Removed
* **Breaking:** Remove the Mesa Git Server API. Users can no longer use vanilla Git clients with Mesa. Our first-class Git sync feature continues to function as usual.
* **Breaking:** Remove the Mesa MCP server
### Fixed
* Fix a bug causing older MesaFS FUSE clients to fail when using private-key-signed access tokens
## \[0.45.0] - 2026-08-11
### Changed
* Stop recording API-key usage during authentication
### Removed
* Remove stale last-triggered values from webhook target views
### Fixed
* Fix gRPC not-found responses interrupting unrelated VCS requests on shared HTTP/2 sessions
* Fix HTTP/2 stream resets crashing API workers under concurrent VCS traffic
* Fix canceled VCS requests creating excess HTTP/2 sessions and stalled streams
* Fix VCS request stalls after task replacement by draining watch streams and replacing stale HTTP/2 sessions
* Fix API-key usage updates blocking authenticated requests under concurrency
* Fix concurrent writes returning internal errors instead of retrying transaction conflicts
* Fix VCS request stalls when long-lived watch streams exhaust shared HTTP/2 connections
## \[0.44.1] - 2026-08-09
### Changed
* Re-shape VCS `ApplyOps` streams at the gateway to the server-advertised operation and message-size limits, including requests from older clients
### Added
* Add the maximum encoded request-message size to the existing VCS `ApplyOps` policy response
### Fixed
* Replace VCS upstream HTTP/2 connections after `ENHANCE_YOUR_CALM` resets and spread requests across four long-lived connections
## \[0.44.0] - 2026-08-07
### Changed
* Limit signing-key commit attribution to 100 ordered authors
* Name `allow_backwards` rather than the CLI-only flag spelling in the `BOOKMARK_MOVE_HISTORY_REWRITE` message
* Allow Git sync to move rewritten branches and tags with exact old-OID leases
* Allow Git pushes to move rewritten branches and tags with exact old-OID leases
* Add a bounded online command to repair stale and same-Change physical ancestry
* Keep lightweight and annotated Git tags attached when their target Change commits are rewritten and omit invalid signatures from rewritten tag objects
* Rebase descendant Changes recursively when a parent Change evolves, preserving committed content and bookmark positions while rejecting descendants with pending writes
### Added
* Add transparent pagination for Content API directory listings
* Report the refused bookmarks in `details.bookmark_names` on `BOOKMARK_MOVE_HISTORY_REWRITE` errors
* Use signing-key authors for Mesa commit writes and preserve Git authors and co-authors during pushes and sync
### Fixed
* Fix Git sync rewrite races and partial branch/tag updates with atomic leased retries
* Fix clean ancestor bookmark merges that hang during conflict checks
* Fix duplicate changes across paginated merge histories with tied or nonmonotonic commit times
## \[0.43.0] - 2026-08-02
### Changed
* **Breaking:** Require REST and MCP bookmark moves to advance history unless `allow_backwards` is set
* Add atomic multi-bookmark moves to the VCS service
### Added
* Support private-key-signed tokens for Mesa Git Server APIs and MCP
### Fixed
* Fix webhooks not firing for repository mutations made through the Mesa CLI or MesaFS
* Retry transient webhook delivery failures
* Prevent VCS object reads from returning blob or tree content not registered to the requested repository
* Prevent `SetChangeBase` from overwriting a change that evolved while its new commit was being computed
## \[0.42.0] - 2026-07-16
### Added
* Add dashboard management for organization signing keys, including browser-local key generation, two-key rotation, revocation, and last-used activity
### Fixed
* Fix the GET change RPC to report `files` and `updated_at` correctly
## \[0.40.0] - 2026-07-12
### Changed
* **Breaking:** Align repository names with GitHub's naming rules — a single path segment of letters, numbers, periods, hyphens, and underscores. Slash-separated (nested) names are no longer accepted, and `.`, `..`, and names ending in `.git` are rejected. Periods are newly allowed (e.g. `.dotfiles`, `sdk-v1.2`)
* **Breaking:** Make repository names case-insensitively unique per organization — names keep the casing they were created with, but `MyRepo` and `myrepo` now refer to the same repository and can no longer coexist
## \[0.39.0] - 2026-07-08
### Changed
* **Breaking:** Reserve tag keys starting with `$` for filter operators — creating or updating repository or API key tags with a `$`-prefixed key now returns `INVALID_REQUEST`
### Added
* Add structured JSON tag filters to `GET /v1/:org/repos`, with `$`-prefixed case-insensitive operators (`$and`, `$or`, `$not`, `$eq`, `$in`, `$contains`, `$starts_with`, `$ends_with`, `$exists`) and case-insensitive tag matching
### Deprecated
* Deprecate legacy comma-separated repo tag filters in favor of structured JSON tag filters
## \[0.37.0] - 2026-06-17
### Added
* Include a `trace_id` field in 5xx error responses. Quote it when reporting an issue and Mesa support can pull up the exact failing request.
## \[0.36.0] - 2026-06-09
### Added
* Accept access tokens (JWTs) as Bearer credentials on all v1 REST endpoints and the VCS gateway, anywhere an API key is accepted. Tokens are signed locally from an API key, with no token endpoint to call. Each request re-verifies the token against its signing key and clamps its scopes and repository access to the key's current state, so disabling or revoking the key invalidates every token it signed
## \[0.35.0] - 2026-06-07
### Added
* Add the Mesa MCP server, which lets you connect your coding agents and other AI tools to Mesa. Through it, an agent can:
* Create repositories, and list or look up existing ones
* Browse directories, read file contents, and inspect path metadata at any change or commit
* Inspect commit and change history, including a change's full evolution, and diff any two changes or commits
* Create a change, write, edit, or delete files in it, then snapshot the result into durable history
* List, create, move, delete, and merge bookmarks to publish work
* Add `https://api.mesa.dev/mcp` as a custom connector, sign in, and choose what access to grant to use the MCP server
### Fixed
* Change writes that hit a concurrent modification now return `409 Conflict` instead of `500`.
## \[0.34.0] - 2026-06-01
### Added
* Add shallow clone and partial clone support for Git fetch over protocol v2, including `--depth`, `--deepen`, `--unshallow`, `--filter=blob:none`, `--filter=blob:limit=`, and `--filter=tree:0`.
* Add `ref-in-want` support for Git fetch over protocol v2, allowing clients to request refs directly during fetch.
## \[0.33.0] - 2026-05-26
### Changed
* **Breaking:** Git fetch (`git-upload-pack`) now speaks **Git protocol v2 only**. `info/refs?service=git-upload-pack` always returns a v2 capability advertisement (`version 2`, `ls-refs=unborn peel`, `fetch=wait-for-done`, `object-format=sha1`, etc.); `POST /git-upload-pack` accepts `command=ls-refs` and `command=fetch` request framing. Pre-v2 Git clients can no longer fetch from Mesa — minimum supported client is **Git 2.18** (≥ 2.26 strongly recommended, since 2.26 enables v2 by default). Push (`git-receive-pack`) is unchanged. The `Git-Protocol` request header is now intentionally ignored on the upload-pack route.
* Change responses now report parent change IDs instead of commit SHAs of prior versions of the same change.
## \[0.31.0] - 2026-05-21
### Changed
* **Breaking:** Rename the content-metadata surface from `xattrs` to `metadata`. `GET /v1/:org/:repo/content` responses now expose a `metadata` object instead of `xattrs`; directory listings filter via repeatable `metadata[]=` deepObject params instead of `xattr=:`; and keys are bare (the `user.mesa.*` namespace prefix is dropped).
## \[0.30.0] - 2026-05-19
### Added
* Add optional `xattrs` metadata to `GET /v1/:org/:repo/content` responses. Files, symlinks, directories, and directory entries now include UTF-8 string extended attributes under `user.mesa.*` when present.
* Add repeatable `xattr=:` filters to `GET /v1/:org/:repo/content` directory listings, returning entries that match all supplied xattr filters.
### Changed
* Change `POST /v1/:org/:repo/changes` and `PATCH /v1/:org/:repo/changes/:change_id` message handling so omitted `message` creates a change with no description on POST and preserves the existing description on PATCH, while explicit string values are accepted including `""` to clear a description.
* Change `POST /v1/:org/:repo/bookmarks/merge` to accept an optional `message`; omitted `message` uses a generated merge description, while explicit string values are accepted including `""` for no description.
## \[0.29.2] - 2026-05-17
### Added
* Add `GET /v1/:org/:repo/bookmarks/:bookmark` for direct bookmark lookup by name, and add `glob` filtering to `GET /v1/:org/:repo/bookmarks`
* Add optional `ref_globs` filters to `POST /v1/:org/:repo/upstream/syncs`. Omit it to sync all supported branches and tags, or pass branch/tag glob strings such as `{ "branches": "main" }`
## \[0.29.0] - 2026-05-15
### Added
* Add `upstream` field to repository responses (`{ url, auth_kind, latest_sync } | null`). Upstreams are added, replaced, or removed via `POST /v1/:org/repos` and `PATCH /v1/:org/:repo`. Auth is write-only: pass `{ kind: 'token', token, token_username? }` or `{ kind: 'username_password', username, password }`; responses expose only `auth_kind`
* Add `POST /v1/:org/:repo/upstream/syncs` to enqueue a sync with the repository upstream. Pass `{ "direction": "pull" }` or `{ "direction": "push" }`. Returns the `sync` row; the worker processes it asynchronously. Read `upstream.latest_sync`, call `GET /v1/:org/:repo/upstream/syncs/:syncId`, list `GET /v1/:org/:repo/upstream/syncs`, or subscribe to `sync.{queued,in_progress,completed,failed}` webhooks
* Add `GET /v1/:org/:repo/upstream/syncs` and `GET /v1/:org/:repo/upstream/syncs/:syncId` for repository upstream sync history
* Add `sync.queued`, `sync.in_progress`, `sync.completed`, and `sync.failed` webhook event types covering the lifecycle of a sync
### Changed
* Change webhook delivery `User-Agent` from `Depot-Webhook/1.0` to `Mesa-Webhook/1.0`
### Removed
* Remove deprecated `X-Depot-Event`, `X-Depot-Delivery`, and `X-Depot-Signature` webhook delivery headers
### Fixed
* Empty repos created via `POST /:org/repos` are now usable by mesa clients without manual seeding — the server lazily materializes an initial change on first write, advancing the default bookmark off `ZERO_OID` at the same time. `git push` to a fresh repo continues to work as a ref-create without `--force` ([MES-1387](https://linear.app/mesa-dev/issue/MES-1387))
## \[0.28.2] - 2026-05-07
### Changed
* Hide refs that point at (or descend from) an unresolved conflict from `git fetch` and `git ls-remote`. These refs previously failed mid-fetch
## \[0.28.1] - 2026-05-06
### Added
* Add `X-Mesa-Event`, `X-Mesa-Delivery`, and `X-Mesa-Signature` webhook delivery headers alongside existing `X-Depot-*` headers
### Changed
* Rename raw-content response headers `X-Depot-Blob-Hash` and `X-Depot-Blob-Size` to `X-Mesa-Blob-Hash` and `X-Mesa-Blob-Size`
* Deprecate `X-Depot-Event`, `X-Depot-Delivery`, and `X-Depot-Signature` webhook delivery headers. Receivers must migrate to the `X-Mesa-*` equivalents by 2026-05-11 or signature verification will fail when the legacy headers stop being sent
## \[0.28.0] - 2026-05-03
### Fixed
* Return 409 Conflict instead of 500 Internal Server Error on concurrent bookmark writes.
## \[0.27.0] - 2026-04-30
No user-facing changes.
## \[0.26.0] - 2026-04-29
### Added
* Add webhook management endpoints for creating, updating, and deleting repository webhooks.
* Add git tag support in receive-pack, info/refs, and upload-pack.
# TypeScript SDK Changelog
Source: https://docs.mesa.dev/content/changelog/typescript-sdk
Release history for the TypeScript SDK.
All notable changes to this project will be documented in this file.
The format is based on [Common Changelog](https://common-changelog.org/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 0.48.0 - 2026-09-10
### Changed
* **Breaking:** Rename `FsLayoutAuthor` to `Author` for commit attribution across commit-producing operations
* **Breaking:** Rename the access-token-only `MesaFileSystemConfig.credential` field to `accessToken` and report an empty value with `MissingAccessTokenError` (`MISSING_ACCESS_TOKEN`)
* **Breaking:** Rename `MissingCredentialError` to `MissingPrivateKeyError` and its code from `MISSING_CREDENTIAL` to `MISSING_PRIVATE_KEY`
* **Breaking:** Consolidate `LayoutSpec` and the prepared `Layout` wrapper into a validated, plain `Layout` map; serialize it with `JSON.stringify()`
### Added
* Add `authors` and `authored_at` to `change.created` and `change.evolved` webhook events; `email` is `null` when none was supplied
* Add `authors` and `authored_at` to change results
### Removed
* **Breaking:** Remove the deprecated singular `author` from change results and from normalized `change.created` / `change.evolved` webhook events; read `authors` and `authored_at` instead. Raw `@mesadev/rest` responses keep `author` for API compatibility
* **Breaking:** Remove `MesaFileSystem.create()` and stop exporting `MesaFileSystem` as a value; it remains exported as a type for annotating the result of `mesa.fs({ layout, authors }).mount()`, which is now the only way to open a filesystem. `MesaFileSystem.createAsync()` and `MesaFileSystem.validateLayout()` are no longer reachable from the package entry point
* **Breaking:** Remove legacy comma-separated repository tag filters from `mesa.repos.list()`; pass a structured `RepoTagFilter` object instead
* **Breaking:** Remove positional private-key, nested `auth`, and access-token construction from `Mesa`; construct with `new Mesa()` or `new Mesa({ privateKey, ...options })`, and pass scoped tokens to the CLI, mounted MesaFS environments, or direct REST calls instead
* **Breaking:** Remove API-key management methods and the deprecated MesaFS `apiKey` configuration alias
* **Breaking:** Remove `mesa.tokens.create()`. Mint through a layout definition instead: `mesa.fs({ layout, ttl }).token()`, which scopes the token to the repositories the layout declares
## \[0.47.1] - 2026-08-16
### Fixed
* Fix slow `mount()` initialization for layouts that specify a checked-out revision with `at`, and allow concurrent `mount()` calls to initialize in parallel
## \[0.47.0] - 2026-08-14
### Changed
* **Breaking:** Change the app-mount Bash default working directory from `/home/user` to `/`
## \[0.46.0] - 2026-08-13
### Changed
* **Breaking:** Replace the `mesa.fs.mount({ repos })` mount syntax with the new `mesa.fs({ layout, ... }).mount(...)` syntax
* **Breaking:** Remove `FsMountReposOptions`. `mount()` now only accepts `cache` and `telemetry`; pass `ttl` and `authors` to `mesa.fs()` instead
* **Breaking:** Change `repo()` to select a checkout revision with the `at` field instead of `bookmark` / `changeId`
### Added
* Add `branchedFrom` to `repo()` to fork a new change the first time the mount opens the repo
### Removed
* **Breaking:** Removed the async `mesa.resolveOrg()` method. The organization slug is now available synchronously via `mesa.org.slug`.
* **Breaking:** Remove `org` from the TypeScript SDK constructor and resource methods; the SDK derives the organization from the credential
* **Breaking:** Remove `vcsUrl` option from the client configuration
* **Breaking:** Replace all singular `author` options with a plural `authors`
### Fixed
* Fix `repo()` `at` pins being overridden when the organization's configuration forks the repo on open
## \[0.45.0] - 2026-08-11
### Changed
* Point package source and issue links to the public SDK repository
* **Breaking:** Remove top-level `RepoConfig.bookmark`, `RepoConfig.changeId`, and `RepoConfig.readOnly` (and the deprecated `FromRevision` type). Use `at` / `branchedFrom` and `mode` instead
* **Breaking:** Resolve `fs.change.new({ bookmark })` (and `edit` / mount `at.bookmark`) strictly against `refs/heads/`. A string that is not a bookmark no longer falls through server-side revspec resolution to a change id; pass `changeId` to fork from a change
### Added
* Add mount `RepoConfig` fields `mode`, `at`, and `branchedFrom` to open an existing revision or fork a new revision at mount time (create change → optional `as.bookmark` → publish checkout; omit `as.bookmark` for an anonymous tip)
### Removed
* **Breaking:** Remove API-key authentication from the SDK, including `apiKey`, `MESA_API_KEY`, legacy HS256 token minting, and `MissingApiKeyError`; use `privateKey` or `auth.accessToken` instead
## \[0.44.1] - 2026-08-09
### Changed
* Split native filesystem write batches to the server-advertised VCS operation and message-size limits
## \[0.44.0] - 2026-08-07
### Changed
* Resolve the organization for `auth.accessToken` locally from its JWT issuer and reject opaque or malformed tokens instead of making an implicit `/whoami` request
* Limit signing-key token and commit attribution to 100 ordered authors
* Deprecate the `apiKey` client option, the `MESA_API_KEY` environment fallback, and the `apiKeys` resource in favor of private keys, and the `org` client option (the organization is derived from the credential); API keys remain supported
* Change native addon loading to fail with the actionable `Unable to load mesafs-napi native addon` error (original failure attached as `cause`) when the package is installed but no platform binary is present
### Added
* Allow private-key authentication to carry ordered commit authors, with explicit authors required for token creation, commit writes, and mounts
* Add custom mount layouts, created by calling `mesa.fs({ layout, ttl })`
* Add `token()` and `layout()` on the FS mount definition for serializing a token and layout file that can be passed into a sandbox
* Add `fs.change.checkpoint({ repo, message? })` to flush pending writes (serializing concurrent writes and tree mutations in the same repo), optionally describe the current change, create a new descendant change, and advance bookmarks onto that descendant (requires the checkout to already be on a bookmark). Concurrent bookmark-tip races raise a retryable conflict (distinct from not-on-a-bookmark and from a half-applied checkout failure). Omit `message` to preserve the existing description; pass `message: ''` to clear it; any other string overwrites. Returns `CheckpointResult` with `savedChangeOid` (saved change) and `activeChangeOid` (now-active empty descendant)
## \[0.43.0] - 2026-08-02
### Added
* Allow private keys to authenticate normal SDK requests and MesaFS mounts with `new Mesa({ privateKey })`. Private-key clients can also mint scoped access tokens locally with `tokens.create()`, and those tokens can authenticate another client through `auth.accessToken`
* Add repository ID restrictions to locally signed access tokens with `mesa.tokens.create({ repo_ids: [...] })`
### Changed
* **Breaking:** Require REST and native filesystem bookmark moves to advance history by default; pass `allow_backwards` or `allowBackwards` to permit intentional backward or sideways moves
* **Breaking:** Change `Mesa.apiKey` from `string` to `string | undefined`; it remains a string for API-key clients and is undefined for private-key and access-token clients
## \[0.39.0] - 2026-07-08
### Added
* Add structured tag filter objects to `mesa.repos.list()`, with `$`-prefixed case-insensitive operators (`$and`, `$or`, `$not`, `$eq`, `$in`, `$contains`, `$starts_with`, `$ends_with`, `$exists`) and case-insensitive tag matching
### Deprecated
* Deprecate legacy comma-separated repo tag filters in favor of structured tag filter objects
## \[0.38.0] - 2026-06-22
### Fixed
* Fix files written through the SDK transiently disappearing during bursts of writes
## \[0.37.0] - 2026-06-17
### Fixed
* Repeated filesystem operations on the same repository are now dramatically faster
## \[0.36.0] - 2026-06-09
### Added
* Add `mesa.tokens.create()` to sign a self-expiring access token (JWT) locally from an API key, scoped to a subset of the key's scopes and to repositories given as full `org/repo` names. Signing is local with no network call; a `ttl_seconds` outside 1 second to 24 hours raises `InvalidOptionsError`
* Add a `ttl` option (seconds) to `fs.mount()` to set how long the mount lasts. The mount signs one access token for that lifetime and expires with it. Defaults to 1 hour, capped at 24 hours
* Add `MesaFileSystemConfig.credential` as the preferred name for the bearer-credential field, which accepts an access token (JWT) or an API key
* Add `MissingCredentialError` as the preferred name for the error thrown when the `Mesa` constructor is given no API key (and `MESA_API_KEY` is unset)
### Changed
* `fs.mount()` now signs a short-lived access token locally from your API key instead of creating an ephemeral API key on the server. No server-side credential is created or left behind, and the mount expires on its own when the token does
### Deprecated
* Deprecate `MesaFileSystemConfig.apiKey` in favor of `credential`. The `apiKey` field is still accepted; supply exactly one (`credential` wins if both are set)
* Deprecate `MissingApiKeyError` in favor of `MissingCredentialError`. The old name is retained as an alias of the same class, so `instanceof` checks under either name keep working
### Removed
* Remove the automatic API key cleanup that ran on process exit; `fs.mount()` no longer creates server-side keys
## \[0.35.0] - 2026-06-07
### Changed
* Change `mesa.fs.mount()` native addon loading to use the `@mesadev/mesafs-napi` package while keeping `MESA_NAPI_PATH` as a local override
## \[0.34.0] - 2026-06-01
### Added
* Changes in Mesa are now realtime by default. Concurrent editors mounted on the same change will see one another's modifications within a few seconds.
* Add `MesaFileSystem.subscribe(handler)` for filesystem invalidation callbacks across mounted changes. Call `unsubscribe()` on the returned subscription to stop.
### Removed
* Remove the experimental `multiplayer` filesystem APIs.
## \[0.33.0] - 2026-05-26
### Added
* Add per-repo `readOnly?: boolean` to `RepoConfig` in `fs.mount(...)`. When `true`, the mesa daemon rejects writes to that repo with `EROFS`, so a single mount can carry a mix of writable and read-only repos.
### Changed
* Change responses now report parent change IDs instead of commit SHAs of prior versions of the same change.
### Removed
* **Breaking:** Remove the mount-wide `mode?: 'ro' | 'rw'` option from `fs.mount(...)` and `MesaFileSystem.create(...)`. Set `readOnly` on each `RepoConfig` instead. The minted API key always carries `read` and `write` scopes; read-only enforcement is performed client-side by the mesa daemon
## \[0.32.0] - 2026-05-22
### Added
* Add `multiplayer.subscribe()` for event-emitter style multiplayer event streaming. Returns a `MultiplayerSubscription` that emits `remote-change` (with `generation`) and `peers-changed` (with `count`) events. Call `.unsubscribe()` to stop.
* Add `multiplayer.watch()` for `AsyncIterable`-based multiplayer event streaming. Yields `MultiplayerEvent` objects and supports `AbortSignal` for cancellation.
## \[0.31.0] - 2026-05-21
### Added
* Add `setMetadata` / `getMetadata` / `clearMetadata` on `MesaFileSystem` for reading and writing per-file metadata as plain key/value pairs (e.g. `{ origin: 'notion:page_foo' }`). `setMetadata` merges per key; passing `null` or an empty string as a value deletes that key, so one call can mix sets and deletes. `getMetadata` returns all keys including the read-only `org`/`repo`. `clearMetadata` removes every key on a path.
### Changed
* **Breaking:** Rename the `client.repos.content.get(...)` metadata surface from `xattrs` to `metadata`. Responses now expose `metadata?: Record` instead of `xattrs?`, and directory listings filter via `metadata` key/value pairs instead of `xattr` pairs.
## \[0.30.0] - 2026-05-19
### Added
* Respond to `client.repos.content.get(...)` with optional `xattrs?: Record` metadata for files, symlinks, directories, and directory entries when present.
* Add `xattr` filters to `client.repos.content.get(...)` directory listings, returning entries that match all supplied xattr `name:value` pairs.
* Add `message` support to `fs.change.new(...)` for setting the description of a newly created mounted-filesystem change.
### Changed
* `mesa.changes.create(...)` and `mesa.changes.patch(...)` now accept empty string descriptions. Omit `message` on create to create a change with no description, or omit it on patch to preserve the existing description; pass `""` to create or update a change with no description.
* `mesa.bookmarks.merge(...)` now accepts `message`; omit it to use the generated merge description, or pass `""` to create the merge with no description.
## \[0.29.3] - 2026-05-18
### Fixed
* Make every `mesa.fs` write durable before the operation returns
## \[0.29.2] - 2026-05-17
### Added
* Add `mesa.bookmarks.get({ repo, bookmark })` for direct bookmark lookup by name, and add `glob` filtering to `mesa.bookmarks.list({ repo, glob })`
* Add optional `ref_globs` filters to `mesa.repos.syncUpstream(...)`: omit it to sync all supported branches and tags, or pass branch/tag glob strings such as `{ branches: "main" }`
## \[0.29.1] - 2026-05-16
### Fixed
* Fix `mesa.fs.mount()` native addon loading by restoring platform package entrypoints and falling back to direct `mesa-napi.node` resolution
## \[0.29.0] - 2026-05-15
### Added
* Add `VIRTUAL_ROOT_CHANGE_ID` export for referencing the virtual-root sentinel change id from `@mesadev/sdk`
* Add `mesa.repos.syncUpstream({ repo, direction })` for triggering an sync with a repository's upstream. Returns the `sync` row; the worker processes it asynchronously. Read `repo.upstream.latest_sync`, call `mesa.repos.getUpstreamSync({ repo, syncId })`, list `mesa.repos.listUpstreamSyncs({ repo })`, or subscribe to `sync.{queued,in_progress,completed,failed}` webhooks
* Add `mesa.repos.{getUpstreamSync, listUpstreamSyncs}` for reading repository upstream sync history
* `mesa.repos.create` and `mesa.repos.update` accept an `upstream: { url, auth? }` field to add or replace the repository upstream. The `auth` payload is inline: `{ kind: 'token', token, token_username? }` or `{ kind: 'username_password', username, password }`. `mesa.repos.update` accepts `upstream: null` to remove. Repository responses include the `upstream` configuration with `latest_sync`; secrets are never returned
* Surface `sync.queued`, `sync.in_progress`, `sync.completed`, and `sync.failed` webhook event types
### Changed
* Resolving the default bookmark of a freshly created (never-written-to) repo continues to return the virtual root sentinel `change_id`. The server-side lazy-seed behavior is transparent to SDK consumers — the first write through any path advances the bookmark to a real change ([MES-1387](https://linear.app/mesa-dev/issue/MES-1387))
### Fixed
* Fix REST API errors to throw human-readable `MesaApiError` instances while preserving structured response metadata
### Removed
* **Breaking:** Remove the public `mesa.raw` generated REST operation namespace; use `@mesadev/rest` directly for low-level REST access
## \[0.28.2] - 2026-05-07
### Added
* Add `fs.change.current({ repo })` to query the currently active change on a mounted repo. Returns `ChangeInfo` with `changeId` and `commitOid`.
## \[0.28.1] - 2026-05-06
### Changed
* Rename `SIGNATURE_HEADER` export from `x-depot-signature` to `x-mesa-signature`. Consumers verifying webhook signatures must update to match the new header name.
* Coordinate `MesaFileSystem` cache budgets across instances in a single process;
`disk_cache.max_size_bytes: null` now auto-sizes against system resources instead of being unbounded
### Fixed
* Fix `rg` and other tools that resolve `/..` by clamping `resolve_path` at the filesystem root.
## \[0.28.0] - 2026-05-03
### Added
* Added support for conflicts in the FS interfaces. Conflicting files render `jj`-style conflict
semantics.
### Fixed
* Surface `LOCK_CONFLICT` instead of `INTERNAL_ERROR` on concurrent bookmark writes.
* Fixed race condition in cache deallocation theoretically causing crashes.
## \[0.27.0] - 2026-04-30
No user-facing changes.
## \[0.26.0] - 2026-04-29
### Changed
* **Breaking:** Rename "upstreams" to "remotes" across all SDK interfaces.
### Fixed
* Fix repo list cache not being exercised through the SDK codepath, hitting an uncached API on every
call.
# Authentication
Source: https://docs.mesa.dev/content/concepts/authentication
Understand Mesa private keys, access tokens, and how commits are attributed.
Mesa has two credentials for programmatic access:
1. **Private keys**: long-lived credentials meant for use in trusted environments (ex. your application server).
2. **Access tokens** (JWTs): short-lived tokens meant for use in ephemeral, less-trusted environments (ex. an agent sandbox).
Private keys can mint access tokens, but access tokens cannot be used to mint more credentials.
Keep private keys in trusted infrastructure. Use access tokens anywhere you would be uncomfortable leaving a durable secret behind: agent sandboxes, disposable VMs, container sessions, or per-user agent jobs.
A private key is never sent to Mesa. The SDK signs access tokens with it locally, and Mesa stores only the matching public key, so minting a token requires no network round-trip.
## Basic Usage
Create a key in the **Keys** section of your organization's settings in the [dashboard](https://app.mesa.dev). The private key is shown once, so save it to your secrets manager as `MESA_PRIVATE_KEY` before leaving the page.
Construct SDK clients with the private key in your trusted backend:
```typescript TypeScript theme={null}
import { Mesa } from "@mesadev/sdk";
const mesa = new Mesa({ privateKey: process.env.MESA_PRIVATE_KEY });
```
```python Python theme={null}
import os
from mesa_sdk import Mesa
mesa = Mesa(private_key=os.environ["MESA_PRIVATE_KEY"])
```
## Authors
Every commit in Mesa records who made it. Since agents and humans often work on the same repository, a commit can have several authors: for example the user a session belongs to plus the agent doing the edits.
Each operation that creates commits takes an ordered `authors` list. Private-key layout definitions take one too (`mesa.fs({ layout, authors })`), because writes through the mount become commits.
```typescript TypeScript theme={null}
import { repo } from "@mesadev/sdk";
// The user the work belongs to first, then the agent that did it.
const authors = [
{ name: "Jane Doe", email: "jane@acme.dev" },
{ name: "Acme Agent" }, // email is optional
];
// Writes through an app mount become commits, so the layout definition takes authors.
const fs = await mesa.fs({
layout: {
"/workspace": repo("agent-workspace", { mode: "rw", at: { bookmark: "main" } }),
},
authors,
}).mount();
// Commit operations take the same list.
await mesa.bookmarks.merge({
repo: "agent-workspace",
target: "main",
source: "agent-draft",
authors,
});
```
```python Python theme={null}
from mesa_sdk import repo
# The user the work belongs to first, then the agent that did it.
authors = [
{"name": "Jane Doe", "email": "jane@acme.dev"},
{"name": "Acme Agent"}, # email is optional
]
# Writes through an app mount become commits, so the layout definition takes authors.
async with mesa.fs(
layout={"/workspace": repo("agent-workspace", mode="rw", at={"bookmark": "main"})},
authors=authors,
).mount() as fs:
...
# Commit operations take the same list.
await mesa.bookmarks.merge(
repo="agent-workspace",
target="main",
source="agent-draft",
authors=authors,
)
```
Mesa preserves the order of the `authors` list. The first entry is the primary author; the rest are co-authors. In the example above, Jane owns the commits and the agent is credited alongside her.
## Minting Access Tokens
Mint a token through a layout definition when you need to hand an access token to
another environment, such as a sandbox where the Mesa CLI will run. The layout
defines both the mount's complete visible path tree and the authority of the token: it
reaches the repositories the layout declares and nothing else. The token carries
its own authors, so anything the sandbox writes is attributed the same way.
```typescript TypeScript theme={null}
import { repo } from "@mesadev/sdk";
const workspace = mesa.fs({
layout: { "/workspace": repo("agent-workspace", { mode: "rw" }) },
authors: [{ name: "Mesa Bot", email: "mesa-bot@example.com" }],
ttl: 60 * 60, // 1 hour; max 4 hours
});
const { token } = await workspace.token();
const layoutJson = JSON.stringify(workspace.layout(), null, 2);
// Copy layoutJson to layout.json in the sandbox, then run:
// MESA_ACCESS_TOKEN=... mesa mount --layout=layout.json --daemonize
```
```python Python theme={null}
import json
from mesa_sdk import repo
workspace = mesa.fs(
layout={"/workspace": repo("agent-workspace", mode="rw")},
authors=[{"name": "Mesa Bot", "email": "mesa-bot@example.com"}],
ttl=60 * 60, # 1 hour; max 4 hours
)
minted = await workspace.token()
layout_json = json.dumps(workspace.layout(), indent=2)
# Copy layout_json to layout.json in the sandbox, then run:
# MESA_ACCESS_TOKEN=... mesa mount --layout=layout.json --daemonize
```
The same definition serves the mount. Serialize `workspace.layout()` with
`JSON.stringify(...)` in TypeScript or `json.dumps(...)` in Python,
write it into the sandbox and start the CLI with `mesa mount --layout`, and the
mount presents exactly the paths the token can reach. A `mode` of `ro` grants
read only access to that repository; `rw` grants read and write access.
Repositories outside the layout are not accessible.
A layout-scoped token is the only token the SDKs mint. There is no way to
export an organization-wide access token: a token always names the repositories
it can reach. Code that needs organization-wide authority — creating
repositories, administering webhooks — must hold the private key and use
`new Mesa({ privateKey })`, which signs a short-lived organization-scoped
token for each request it makes and never hands one out.
See [Daytona](/content/integrations/sandboxes/daytona) for a full end-to-end example of this flow with a real sandbox provider.
The layout defines the token's complete repository authority, one permission per
repository. A `mode` of `ro` grants `read-repo`; `rw` grants `write-repo`, which
also allows reads. Repositories outside the layout are not accessible, and a
access token can never create repositories.
Tokens default to a 15 minute TTL, have a maximum TTL of 4 hours, and are not refreshed automatically. A mount uses one token for its whole lifetime, so choose a TTL that covers the mount's work.
Inside the receiving environment, the Mesa CLI reads the token from `MESA_ACCESS_TOKEN`. Mounted MesaFS environments and direct REST requests also accept access tokens. TypeScript and Python `Mesa` clients require a private key and do not accept access tokens.
# Filesystem
Source: https://docs.mesa.dev/content/concepts/filesystem
Understand Mesa's filesystem interface and choose between the POSIX mount and app mount
Mesa's virtual filesystem lets tools interact with repository files directly — no full clone required. Instead of pulling an entire repo to disk, Mesa fetches data on demand and presents it through a standard filesystem interface. Reads and writes go through Mesa's API transparently, so any tool that works with files can work with Mesa repositories.
There are two ways to use the virtual filesystem: **POSIX mount** (FUSE) and **app mount**. Both give you access to the same underlying Mesa filesystem — the difference is how it's exposed to your tools.
## POSIX Mount
The POSIX mount uses [FUSE](https://en.wikipedia.org/wiki/Filesystem_in_Userspace) to mount repositories as real directories on the host. Every process on the machine — editors, language servers, build systems, agents — sees standard files and directories at a mount path like `~/.local/share/mesa/mnt/my-org/my-repo`.
**Use a POSIX mount when:**
* You're working inside a sandboxed environment (isolated Docker container, Daytona micro-VM, etc.)
* You need multiple concurrent processes to access files through native filesystem APIs (ex. multi-processes architures, build systems, compilers, language servers)
* Your agent needs to install external dependencies (`npm install`, `pip install`, etc.) or run arbitrary code
See [POSIX Mount](/content/mesafs/posix-mount) for setup and usage.
## App Mount
The app mount runs entirely in-process via the Python and TypeScript SDKs. Your application gets a Mesa filesystem handle, and can either call filesystem methods (`readFile`, `writeFile`, `readdir`, etc.) directly or use the Mesa-provided emulated bash tool to execute shell commands (`ls`, `cat`, `grep`, `cp`, etc.) against Mesa repositories — no FUSE, no sandbox required.
**Use an app mount when:**
* You have a TS/Python agent running in your own backend and don't want to manage a sandbox
* Your agent only needs to read/write files and run shell commands (no dependency installs or arbitrary binaries)
* You want the simplest possible setup — just `npm install @mesadev/sdk` and go
* You're building with frameworks like Vercel AI SDK, Mastra, or Langchain and want to add a bash tool
See [App Mount](/content/mesafs/app-mount) for setup and usage.
## Choosing between the two
| | POSIX mount (FUSE) | App mount |
| -------------------------- | ----------------------------------------- | ----------------------------------------- |
| **Setup** | Requires FUSE + sandbox | Only requires the Mesa SDK |
| **Install dependencies** | Yes | No |
| **Run arbitrary binaries** | Yes | No (bash builtins + optional Python/JS) |
| **Multi-process access** | Yes | Single process |
| **Environment** | Any (Docker, VMs, local) | Node.js/Python backend |
| **Best for** | Full dev environments, CI, sandbox agents | Lightweight agents, multi-tenant backends |
Both modes support read and write operations and use the same caching and prefetching under the hood.
## Writing to a mount
Whatever revision you mount, typically a bookmark like `main`, or a specific change, that is the change your writes edit, **in place**.
```text theme={null}
@ qzvqqupx main ← mounted here
○ ovknlmro
◆ root
```
A write through the mount amends `qzvqqupx`. What you mounted is what you modified.
Any two writers looking at the same change will see one another's modifications in **real time**.
For example, if two different machines run `mesa mount` on the same `main` bookmark, these two machines will have identical views of the files.
To isolate edits, you can give each its own change with `mesa new`.
The one exception is an empty repository: there is no change to edit yet, so the first write creates the repo's initial change and the default bookmark picks it up.
### Approvals and branching
You may want a flow where a human or agent "drafts" some modifications without touching your canonical state. A typical case might be an agent producing work that a human later approves.
In this case, to avoid prematurely modifying your canonical documents, you should explicitly "branch" by creating a new change before writing.
When using the `fs.change.new` method, providing a bookmark argument tells Mesa which **base change** to branch from; it does not move the bookmark to the new change.
After calling `fs.change.new`, Mesa will switch onto the new change, meaning writes land there.
Bookmarks stay put until you move them with `mesa.bookmarks.move`.
```typescript TypeScript theme={null}
import { repo } from "@mesadev/sdk";
const fs = await mesa.fs({
layout: {
"/workspace": repo("my-repo", { mode: "rw", at: { bookmark: "main" } }),
},
authors: [{ name: "Workspace Agent", email: "agent@example.com" }],
}).mount();
// Fork a new change off the `main` change. Writes on the mount now modify the fork.
// The forked change is unnamed (has no bookmark attached to it).
const result = await fs.change.new({ repo: "my-repo", bookmark: "main", message: "implement new feature" });
// Do some writes...
// Move the bookmark to the new change to "accept" the writes.
await mesa.bookmarks.move({
repo: "my-repo",
bookmark: "main",
change_id: result.changeOid,
});
```
```python Python theme={null}
from mesa_sdk import repo
async with mesa.fs(
layout={"/workspace": repo("my-repo", mode="rw", at={"bookmark": "main"})},
authors=[{"name": "Workspace Agent", "email": "agent@example.com"}],
).mount() as fs:
# Fork a new change off the `main` change. Writes on the mount now modify the fork.
# The forked change is unnamed (has no bookmark attached to it).
change_id = await fs.changes.new("my-repo", bookmark="main", message="implement new feature")
# Do some writes...
# Move the bookmark to the new change to "accept" the writes.
await mesa.bookmarks.move(
repo="my-repo",
bookmark="main",
change_id=change_id,
)
```
```bash CLI theme={null}
# Fork a new change based on main's tip. The mount now edits the fork,
# no bookmark follows it, and main stays put.
mesa new main --repo acme/my-repo
cd ~/.local/share/mesa/mnt/acme/my-repo
# Do some writes...
# Move the bookmark onto the change to "accept" the writes.
mesa bookmark move main --repo acme/my-repo
```
For mounts that should never write at all, declare `mode: "ro"` on the layout repo: writes are rejected with `EROFS` before they can touch any change.
### Switching changes
Switch the mounted repo onto a different existing change at any time.
The common case is switching to a change with some draft work. After calling `fs.change.edit`, writes now modify the change you switched onto.
To specify the change that you want to switch onto, you can call `fs.change.edit` with either a bookmark or a changeId (in case the change doesn't have an attached bookmark).
```typescript TypeScript theme={null}
import { repo } from "@mesadev/sdk";
const fs = await mesa.fs({
layout: {
"/workspace": repo("my-repo", { mode: "rw", at: { bookmark: "main" } }),
},
authors: [{ name: "Workspace Agent", email: "agent@example.com" }],
}).mount();
// Switch to the change `my-feature` points at. Writes modify that change rather than `main`.
await fs.change.edit({ repo: "my-repo", bookmark: "my-feature" });
// OR switch by id -- ex. a fresh `change.new` fork without a bookmark.
await fs.change.edit({ repo: "my-repo", changeId: result.changeOid });
```
```python Python theme={null}
from mesa_sdk import repo
async with mesa.fs(
layout={"/workspace": repo("my-repo", mode="rw", at={"bookmark": "main"})},
authors=[{"name": "Workspace Agent", "email": "agent@example.com"}],
).mount() as fs:
# Switch to the change `my-feature` points at. Writes modify that change rather than `main`.
await fs.changes.edit("my-repo", bookmark="my-feature")
# OR switch by id -- ex. a fresh `change.new` fork without a bookmark.
await fs.changes.edit("my-repo", change_id=change_id)
```
```bash CLI theme={null}
# Switch to the change `my-feature` points at. Writes modify that change rather than `main`.
mesa edit my-feature --repo acme/my-repo
# OR switch by id -- ex. a fresh `change.new` fork without a bookmark.
mesa edit "$CHANGE_ID" --repo acme/my-repo
```
See [Versioning](/content/concepts/versioning) for more on how changes and bookmarks work in Mesa.
## Realtime
MesaFS reads and writes are realtime by default. Mounts on the same change can see each other's edits, even when they are running on different hosts.
The realtime boundary is the active change, not the repository as a whole. Use separate changes when writers should work in isolation, and use the same change when you want collaborative reads and writes.
For more details, see [Realtime](/content/mesafs/advanced/realtime).
# Organizations
Source: https://docs.mesa.dev/content/concepts/organizations
Manage organizations and team membership in Mesa.
Everything in Mesa lives inside an organization. Repositories, public keys, webhooks, and members are all scoped to a single org. You can belong to multiple orgs and switch between them from the dashboard.
## Organizations
An organization has a **display name** and a **slug**. The slug is the URL-friendly identifier used in API paths.
### Creating an organization
Your first organization is created during sign-up, or you join one by accepting an invitation. If you haven't done this yet, see [Quickstart](/content/getting-started/quickstart).
To create additional organizations, use the org switcher dropdown in the dashboard sidebar. The creator automatically becomes the **owner**.
Slug rules:
* Lowercase letters, numbers, and hyphens only
* Cannot start or end with a hyphen
* Must be unique across all Mesa orgs
### Organization settings
From **Settings** in the sidebar you can update the display name and see org-level stats for members and repositories.
### Deleting an organization
Only **owners** can delete an organization. Deletion is permanent and cascades to all repositories, public keys, members, and invitations.
### API access
Organization identity is available via the API:
* `GET /whoami` — returns the org tied to the calling access token and its effective scopes
* `GET /{org}` — returns org details by slug
Both require the `read` scope. All other org management (create, update, delete) is dashboard-only.
## Roles
Every member has a role that determines what they can do within the org.
| | Owner | Admin | Member |
| --------------------------------------- | ----- | ----- | ------ |
| **Create, update, delete repos** | Yes | Yes | Yes |
| **Manage changes and bookmarks** | Yes | Yes | Yes |
| **View repository content and history** | Yes | Yes | Yes |
| **Manage public keys** | Yes | Yes | No |
| **Manage webhooks** | Yes | Yes | No |
| **Invite and remove members** | Yes | Yes | No |
| **View member list** | Yes | Yes | Yes |
| **Update org name and slug** | Yes | Yes | No |
| **Delete org** | Yes | No | No |
Roles are assigned when a member is invited. There is no way to change a member's role after they join — remove and re-invite with the correct role instead.
## Members
View and manage members from the **Members** page in the dashboard sidebar.
### Inviting members
Click **Invite member** to send an email invitation. You choose the role (`member` or `admin`) at invite time. Inviting requires the `admin` role.
* Invitations expire after **48 hours**
* Re-inviting the same email refreshes the existing invitation
* The invitee must use the same email address to accept
### Pending invitations
Pending invitations appear below the member list. Admins and owners can cancel a pending invitation from the overflow menu.
### Removing members
Admins and owners can remove members. The org owner cannot be removed.
# Versioning
Source: https://docs.mesa.dev/content/concepts/versioning
Model document history with changes and bookmarks
Mesa records the history of every document with robust version control. Mesa's model is based on [Jujutsu VCS](https://docs.jj-vcs.dev/latest/) which allows for full Git-compatibility while enabling more efficient, agent-first workflows.
## Core Primitives
### Repository
A repository in Mesa is a folder that has its own version history and permissions.
Automatic versioning lets you view, undo, and redo modifications to documents without the fear of losing work.
```typescript TypeScript theme={null}
const repo = await mesa.repos.create({ name: "project-1234" });
```
```python Python theme={null}
repo = await mesa.repos.create(name="project-1234")
```
```bash CLI theme={null}
mesa repo create project-1234
```
You'll typically want to create many repositories. For example, an app builder that turns user prompts into apps might treat every user app as a separate repository, because
two users building separate apps shouldn't affect one another's version history or documents.
### Changes
The core primitive of Mesa's version history is the `Change`. Think of a change as a logical unit of work or modification to your repository. Each change has a unique identifier
and can contain an arbitrary number of file edits. You can optionally provide a change description.
A repository for an internal dashboard might have a change history like this:
```text theme={null}
@ qzvqqupx 2026-03-16 10:42:31Z
│ Tune forecast widget behavior
○ puqltutt 2026-03-16 10:37:08Z
│ Add forecast widget
○ ovknlmro 2026-03-16 10:30:02Z
│ Seed dashboard layout
○ nuvyytnq 2026-03-16 10:24:19Z
│ Initialize dashboard repo
◆ root
```
Changes are organized in a Directed Acyclic Graph (DAG), the same way commits are in Git. This means that changes can have multiple parents and multiple children, which allows for branching and merging.
You can modify the change DAG with all the operations you know from Git: branching, merging, rebasing, reverting, cherry-picking, etc.
Create a new change on top of an existing one by passing its id as `base_change_id`. The optional `message` serves as the change description.
```typescript TypeScript theme={null}
import { Buffer } from "node:buffer";
const change = await mesa.changes.create({
repo: "project-1234",
base_change_id: repo.head_change_id,
message: "Add forecast widget",
authors: [{ name: "Agent", email: "agent@acme.dev" }],
files: [
{
path: "src/widgets/forecast.ts",
action: "upsert",
content: Buffer.from("// forecast widget").toString("base64"),
encoding: "base64",
},
],
});
```
```python Python theme={null}
import base64
from mesa_sdk import FileUpsert
change = await mesa.changes.create(
repo="project-1234",
base_change_id=repo.head_change_id,
message="Add forecast widget",
authors=[{"name": "Agent", "email": "agent@acme.dev"}],
files=[
FileUpsert(
path="src/widgets/forecast.py",
content=base64.b64encode(b"# forecast widget").decode(),
)
],
)
```
From inside the virtual filesystem, you can also create a new empty change on top of a bookmark and start editing files directly:
```typescript TypeScript theme={null}
// Create a new change from the tip of `main` and switch to it
await fs.change.new({ repo: "project-1234", bookmark: "main" });
const current = await fs.change.current({ repo: "project-1234" });
// Or switch to an existing change without creating a new one
await fs.change.edit({ repo: "project-1234", changeId: current.changeId });
```
```python Python theme={null}
# Create a new change from the tip of `main` and switch to it
await fs.changes.new("project-1234", bookmark="main")
current = await fs.changes.current("project-1234")
# Or switch to an existing change without creating a new one
await fs.changes.edit("project-1234", change_id=current.change_id)
```
```bash CLI theme={null}
# Create a new change from the tip of `main` and switch to it
mesa new main --repo acme/project-1234
# Or switch to an existing change without creating a new one
mesa edit "$CHANGE_ID" --repo acme/project-1234
```
When you mount MesaFS, your writes directly affect the
revision you've mounted at, ex `main`.
To keep work separate, create a new change first with `mesa new`.
See [Writing to a Mount](/content/concepts/filesystem#writing-to-a-mount) for more details.
### Bookmarks
Git's "branches" represent a new unit of work plus a name for that work. In Mesa's JJ-based model, units of work (Changes)
are not required to have human-readable names, however, you have the option to assign these using Bookmarks.
A Bookmark is a lightweight pointer to a specific change in the change DAG that allows you to use reference that change more easily.
Virtually every Mesa API gives you the choice to reference a change by either its ID or an associated bookmark.
Bookmarks are mutable and can be moved from one change to another.
Every repository on mesa has a default bookmark, which represents the canonical state of the project. Conventionally, the default bookmark is called `main`, although this is configurable when creating a repository.
```text theme={null}
@ qzvqqupx 2026-03-16 11:02:11Z feature/widget
│ Polish widget interactions
│ ○ puqltutt 2026-03-16 10:58:40Z main
├─╯ Prepare baseline release
○ ovknlmro 2026-03-16 10:30:02Z
│ Seed dashboard layout
○ nuvyytnq 2026-03-16 10:24:19Z
│ Initialize dashboard repo
◆ root
```
In the version tree above, we can see the main bookmark pointing to the `puqltutt` change and the `feature/widget` bookmark pointing to the `qzvqqupx` change.
To create a bookmark at a given change and move it onto a newer change later:
```typescript TypeScript theme={null}
// Create a bookmark pointing at a change
await mesa.bookmarks.create({
repo: "project-1234",
name: "feature/widget",
change_id: change.id,
});
// Move an existing bookmark to a different change
await mesa.bookmarks.move({
repo: "project-1234",
bookmark: "main",
change_id: change.id,
});
```
```python Python theme={null}
# Create a bookmark pointing at a change
await mesa.bookmarks.create(
repo="project-1234",
name="feature/widget",
change_id=change.id,
)
# Move an existing bookmark to a different change
await mesa.bookmarks.move(
repo="project-1234",
bookmark="main",
change_id=change.id,
)
```
```bash CLI theme={null}
# Create a bookmark pointing at a change
mesa bookmark create feature/widget --repo acme/project-1234 --revision "$CHANGE_ID"
# Move an existing bookmark to a different change
mesa bookmark move main --repo acme/project-1234 --to "$CHANGE_ID"
```
To integrate the work on one bookmark into another, merge them. Mesa fast-forwards when possible and creates a merge commit when histories have diverged:
```typescript TypeScript theme={null}
const result = await mesa.bookmarks.merge({
repo: "project-1234",
target: "main",
source: "feature/widget",
authors: [{ name: "Agent", email: "agent@acme.dev" }],
delete_source: true,
});
console.log(result.merge_type); // 'merge_commit' | 'no_op'
```
```python Python theme={null}
result = await mesa.bookmarks.merge(
repo="project-1234",
target="main",
source="feature/widget",
authors=[{"name": "Agent", "email": "agent@acme.dev"}],
delete_source=True,
)
print(result.merge_type) # 'merge_commit' | 'no_op'
```
### Conflicts
Conflicts work exactly how they do in JJ. This is mostly the same as Git, with the sole exception that conflicts are non-blocking.
A change can be in a conflicted state and you can resolve the conflicts later or in some cases ignore them entirely. This prevents
your agents from getting stuck and gives you maximum flexibility in how to handle conflicts.
A conflict occurs when you've modified the same part of the same file in different ways on two separate branches and then try to merge them. We have multiple ways to resolve them. See [dealing with conflicts](#dealing-with-conflicts) for more details.
## Dealing with conflicts
A conflict happens when two branches start from the same base change and edit the same location differently
Example: two separate branches edit `overview.json` and change the `title` property:
**Before any merge**
Both Chat A and Chat B have edited `overview.json` in the same place.
```text theme={null}
@ qzvqqupx 2026-03-16 11:12:04Z feature/chat-a
│ title = "Revenue + Forecast"
│ ○ puqltutt 2026-03-16 11:10:27Z feature/chat-b
├─╯ title = "Revenue Q4 Summary"
○ ovknlmro 2026-03-16 10:58:40Z main
│ title = "Revenue Overview"
◆ root
```
**Chat B merges first**
Chat B merges first and creates a new change on top of the main bookmark.
```text theme={null}
@ qzvqqupx 2026-03-16 11:12:04Z feature/chat-a
│ title = "Revenue + Forecast"
○ puqltutt 2026-03-16 11:10:27Z main
│ title = "Revenue Q4 Summary"
○ ovknlmro 2026-03-16 10:58:40Z
│ title = "Revenue Overview"
◆ root
```
Now, if we wanted to merge `feature/chat-a` into main Mesa will return an error because the new merge commit would be conflicted.
Because conflicts are non-blocking, a merge can produce a conflicted change that you resolve later. The conflicted state would look like this:
```text theme={null}
x zxoosnnp 2026-03-16 11:13:52Z main
│ (conflict) merge feature/chat-a into main
├─╮
│ ○ qzvqqupx 2026-03-16 11:12:04Z feature/chat-a
│ │ title = "Revenue + Forecast"
○ │ puqltutt 2026-03-16 11:10:27Z
│ │ title = "Revenue Q4 Summary"
├─╯
○ ovknlmro 2026-03-16 10:58:40Z
│ title = "Revenue Overview"
◆ root
```
`feature/chat-a` bookmark still points to `qzvqqupx` (branched from original main change `ovknlmro`), while the conflicted merge change `zxoosnnp` combines both parents (`qzvqqupx` and `puqltutt`).
## Comparing to Git and JJ
Mesa is Jujutsu-based. You can essentially treat changes like commits and bookmarks like branches, but there are some key differences:
* There's no staging area like in Git. You are always editing an existing change and changes can evolve as you edit them.
When you create a new change in Mesa it is initially empty and then you can edit and modify files at that change.
This is ideal for agents running in sandboxes because any edits they make are automatically persisted to a very specific place in the version
tree rather than being saved in some dangling staging area.
* Bookmarks are like branches in Git but lighter weight. They do not automatically move from one change to another but can be moved manually. The consequence of this is that you do not need to explicitly think about
branching ahead of time. If you have a `main` bookmark, you can just create a new change from it and that's effectively an unnamed branch. You can name it later or leave it unnamed and create new changes on top of it or
just move the main bookmark to point at the new change.
Here's a handy table to explain how Mesa version concepts map to Git and Jujutsu.
| Mesa Concept | Git | JJ |
| ------------ | -------- | -------- |
| Repo | Repo | Repo |
| Change | Commit | Change |
| Bookmark | \~Branch | Bookmark |
### Syncing to GitHub & GitLab
Mesa can sync with arbitrary upstream git repositories including ones hosted on GitHub or GitLab. This is useful if you want to create a Mesa repository based on a template repo that lives in GitHub or use Mesa mounts as a faster way to access a GitHub-hosted repo.
Add an upstream to a Mesa repository, then trigger a sync via `syncUpstream` in the SDK or the upstream syncs REST API. See [GitHub](/content/integrations/github/sync) for setup, auth options, and sync observability.
## Next steps
* See [Filesystem](/content/concepts/filesystem)
* See [Usage Patterns](/content/usage-patterns/overview)
# Webhooks
Source: https://docs.mesa.dev/content/concepts/webhooks
Programmatically subscribe to events on your organization's repositories.
Webhooks allow you to receive notifications of changes to your organization's repositories in real time. A **webhook target** represents an `https://` URL to which Mesa will send these notifications, in the form of HTTP POST requests. Each target is scoped to your organization, can subscribe to multiple event types, and can optionally be restricted to a subset of repositories.
## Create a webhook target
```typescript TypeScript theme={null}
import { Mesa } from "@mesadev/sdk";
const mesa = new Mesa({ privateKey: process.env.MESA_PRIVATE_KEY });
const target = await mesa.webhookTargets.create({
name: "My App - Prod", // Optional human-readable label.
url: "https://acme.dev/webhooks/mesa",
events: ["push", "change.created", "change.evolved"],
// Omit `repo_ids` for an org-wide target.
repo_ids: ["repo_abc123"],
});
// The secret is returned once on create. Save it to verify signatures later.
console.log(target.secret);
```
```python Python theme={null}
import os
from mesa_sdk import Mesa
mesa = Mesa(private_key=os.environ["MESA_PRIVATE_KEY"])
target = await mesa.webhook_targets.create(
name="My App - Prod", # Optional human-readable label.
url="https://acme.dev/webhooks/mesa",
events=["push", "change.created", "change.evolved"],
# Omit `repo_ids` for an org-wide target.
repo_ids=["repo_abc123"],
)
# The secret is returned once on create. Save it to verify signatures later.
print(target.secret)
```
Store `target.secret` in your secret manager. The SDK and REST API only return it once, here on create. The dashboard always shows it on the target's detail page, and you can also [rotate](#rotate-the-signing-secret) to mint a new one.
## Supported events
Subscribe to any combination of the following on a target:
| Event | Fires when | `data` shape |
| ------------------ | --------------------------------------------------------------------------------------- | ----------------------------------------- |
| `repo.created` | A repository is created. | `{ repo }` |
| `repo.updated` | A repository's name, default bookmark, or tags change. | `{ repo, before, after }` |
| `repo.deleted` | A repository is deleted. | `{ repo }` |
| `bookmark.created` | A bookmark is created (including the default bookmark on `repo.created`). | `{ bookmark }` |
| `bookmark.deleted` | A bookmark is deleted. | `{ bookmark }` |
| `bookmark.moved` | A bookmark is moved to a different change. | `{ bookmark, from }` |
| `bookmark.merged` | A bookmark is advanced via a merge. | `{ bookmark }` |
| `change.created` | A new change is created. | `{ change }` |
| `change.evolved` | An existing change advances to a new commit (rebase, content edit, message edit, etc.). | `{ change, previous_current_commit_oid }` |
| `push` | A git push lands one or more bookmark updates via `git-receive-pack`. | `{ source, updates, reconciliation }` |
| `sync.queued` | A `syncUpstream` call enqueues a new upstream sync. | `{ sync }` |
| `sync.in_progress` | The sync worker begins running. | `{ sync }` |
| `sync.completed` | A sync finishes with terminal status `completed`. | `{ sync }` |
| `sync.failed` | A sync finishes with terminal status `failed`. | `{ sync }` |
If `events` is omitted on create, it defaults to `["push"]`.
Managing webhook targets requires the `admin` scope. Repo-scoped tokens
can only manage targets whose `repo_ids` is a subset of the repos the
token has access to. They cannot create org-wide targets.
## List webhook targets
Paginated. `limit` is at most 100 and defaults to 100. Pass the previous response's `next_cursor` to fetch the next page; `has_more` indicates whether another page is available.
```typescript TypeScript theme={null}
const { webhook_targets, next_cursor, has_more } = await mesa.webhookTargets.list({
limit: 50,
// cursor: previousResponse.next_cursor,
});
```
```python Python theme={null}
targets = await mesa.webhook_targets.list(
limit=50,
# cursor=previous_response.next_cursor,
)
webhook_targets = targets.webhook_targets
next_cursor = targets.next_cursor
has_more = targets.has_more
```
## Update a webhook target
`PATCH` uses **full-replace** semantics on each field you include. Omitted fields are left unchanged.
* `events`: providing an array replaces the current list of subscribed events in full. There is no incremental add/remove.
* `repo_ids`: providing an array replaces the current repo filter. Pass `null` to clear the filter and make the target org-wide. Empty arrays are rejected.
```typescript TypeScript theme={null}
await mesa.webhookTargets.update({
webhookTargetId: "wh_123",
url: "https://acme.dev/webhooks/mesa-v2",
events: ["push"], // replaces all previously subscribed events
repo_ids: null, // clear the filter -> org-wide
});
```
```python Python theme={null}
await mesa.webhook_targets.update(
webhook_target_id="wh_123",
url="https://acme.dev/webhooks/mesa-v2",
events=["push"], # replaces all previously subscribed events
)
# clear the filter -> org-wide
await mesa.webhook_targets.clear_repo_filter(
webhook_target_id="wh_123",
)
```
The secret is **not** returned by update. See [Rotate the signing secret](#rotate-the-signing-secret) below.
## Delete a webhook target
```typescript TypeScript theme={null}
await mesa.webhookTargets.delete({
webhookTargetId: "wh_123",
});
```
```python Python theme={null}
await mesa.webhook_targets.delete(
webhook_target_id="wh_123",
)
```
## Payload shape
Every delivery shares the same envelope:
```json theme={null}
{
"id": "01HXXXXXXXXXXXXXXXXXXXXXX",
"type": "push",
"occurred_at": "2026-01-28T19:01:05.000Z",
"organization": {
"id": "org_abc123",
"slug": "acme",
"name": "Acme"
},
"repository": {
"id": "repo_abc123",
"name": "vibecode-dashboards",
"url": "https://api.mesa.dev/acme/vibecode-dashboards.git"
},
"data": { ... }
}
```
### `push` event `data`
```json theme={null}
{
"source": "git_receive_pack",
"updates": [
{
"ref": "refs/heads/main",
"bookmark": "main",
"before": "abc123...",
"after": "def456...",
"action": "updated"
}
],
"reconciliation": {
"reconciled_commit_count": 1,
"touched_change_count": 1,
"changes_created_count": 0,
"evolog_inserted_count": 0,
"invalid_change_id_header_count": 0,
"dirty_change_skip_count": 0
}
}
```
`updates[].action` is `created`, `updated`, or `deleted`. `before` is `null` when the action is `created`; `after` is `null` when the action is `deleted`.
### `repo.*` event `data`
```json theme={null}
{
"repo": {
"id": "repo_abc123",
"name": "vibecode-dashboards",
"default_bookmark": "main",
"head_change_id": "...",
"created_at": "2026-01-28T19:01:05.000Z",
"tags": { "team": "ui" }
},
"before": { "name": "...", "default_bookmark": "...", "tags": { ... } },
"after": { "name": "...", "default_bookmark": "...", "tags": { ... } }
}
```
`before` and `after` are only included on `repo.updated`. `repo.created` and `repo.deleted` carry just `repo`.
### `bookmark.*` event `data`
```json theme={null}
{
"bookmark": {
"name": "main",
"is_default": true,
"change_id": "...",
"commit_oid": "..."
},
"from": { "change_id": "...", "commit_oid": "..." }
}
```
`from` is only included on `bookmark.moved`. `bookmark.created`, `bookmark.deleted`, and `bookmark.merged` carry just `bookmark`.
### `change.*` event `data`
```json theme={null}
{
"change": {
"id": "...",
"current_commit_oid": "def456...",
"message": "feat: add foo",
"authors": [
{ "name": "Ada", "email": "ada@example.com" },
{ "name": "Grace", "email": null }
],
"authored_at": "2026-01-28T19:01:05.000Z",
"author": { "name": "Ada", "email": "ada@example.com", "date": "2026-01-28T19:01:05.000Z" },
"committer": { "name": "...", "email": "...", "date": "..." },
"parents": ["abc123..."],
"created_at": "2026-01-28T19:01:05.000Z",
"updated_at": "2026-01-28T19:01:05.000Z"
},
"previous_current_commit_oid": "abc123..."
}
```
`previous_current_commit_oid` is only included on `change.evolved`. `change.created` carries just `change`.
`change.authors` contains every author in supplied order. An author's `email` is `null` when none was supplied. `change.authored_at` is the commit's shared author timestamp. The singular `change.author` remains as a compatibility projection of the first author; its email is an empty string when absent.
### `sync.*` event `data`
```json theme={null}
{
"sync": {
"id": "sync_abc123",
"repo_id": "repo_abc123",
"direction": "pull",
"status": "completed",
"attempt": 1,
"refs": { "mode": "all" },
"stats": { ... },
"error": null,
"created_at": "2026-05-13T19:01:05.000Z",
"started_at": "2026-05-13T19:01:06.100Z",
"finished_at": "2026-05-13T19:01:09.420Z"
}
}
```
`status` reflects the run's state at the moment the event fires: `queued` on `sync.queued`, `in_progress` on `sync.in_progress`, and `completed` or `failed` on the corresponding terminal event. `refs` is the ref-selection policy for the run. `error` is populated on `sync.failed`. `stats` is populated once the worker finishes ingestion or reconciliation.
## Webhook headers
* `Content-Type: application/json`
* `User-Agent: Mesa-Webhook/1.0`
* `X-Mesa-Event: `
* `X-Mesa-Delivery: `
* `X-Mesa-Signature: t=,sha256=`
## Verify signatures
The signature is computed as:
```
HMAC_SHA256(secret, `${timestamp}.${rawBody}`)
```
```typescript TypeScript theme={null}
import { createHmac, timingSafeEqual } from "crypto";
const signatureHeader = request.headers.get("x-mesa-signature");
if (!signatureHeader) throw new Error("Missing signature header");
const parts = Object.fromEntries(
signatureHeader.split(",").map((part) => part.trim().split("="))
);
const timestamp = Number(parts.t);
const signature = parts.sha256;
const body = await request.text();
const expected = createHmac("sha256", process.env.WEBHOOK_SECRET)
.update(`${timestamp}.${body}`)
.digest("hex");
const valid = timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!valid) throw new Error("Invalid signature");
```
```python Python theme={null}
import hashlib
import hmac
import os
signature_header = request.headers.get("x-mesa-signature")
if signature_header is None:
raise ValueError("Missing signature header")
parts = dict(part.strip().split("=", 1) for part in signature_header.split(","))
timestamp = parts["t"]
signature = parts["sha256"]
body = await request.body()
expected = hmac.new(
os.environ["WEBHOOK_SECRET"].encode(),
f"{timestamp}.".encode() + body,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(signature, expected):
raise ValueError("Invalid signature")
```
In your receiver, also reject deliveries whose `t` is more than a few
minutes old (5 minutes is a common choice). This stops an attacker from
replaying a captured request later. The signature stays valid until you
rotate the secret. Time alone doesn't invalidate it.
## Rotate the signing secret
To rotate, open the webhook target's detail page in the dashboard
(**Webhooks** → click the target) and click **Regenerate** in the
**Signing secret** section. Confirm the dialog.
Rotation takes effect immediately:
* Mesa starts signing every subsequent delivery with the new secret.
* Deliveries will fail signature verification at your receiver until
you update it with the new value.
To minimize the failure window, copy the new secret and update your
receiver as soon as you rotate. Rotation requires the `admin` scope, same as create, update, and delete. See the note at the top of this page.
## Delivery behavior
Mesa delivers webhooks with a **10-second timeout** per request. Receivers should respond fast and defer long-running work to a background queue. Anything longer than 10s is treated as a failure.
Mesa automatically retries timeouts, network failures, HTTP `408`, `425`, `429`, and `5xx` responses. Other `4xx` responses and redirects are permanent failures and are not retried. Delivery is at least once after Mesa accepts an event into its queue, so duplicate requests are possible.
The `X-Mesa-Delivery` header contains the stable event ID and remains the same across retries. Use it as an idempotency key on the receiver.
## Local development
Expose your local webhook listener using a tunnel like Tailscale or Cloudflare
Tunnel, then use the public URL when creating the webhook.
# Introduction
Source: https://docs.mesa.dev/content/getting-started/introduction
Mesa is a programmable storage layer and virtual filesystem, built specifically for AI agents and developers building agentic products.
Mesa enables versioning with either simple checkpointing or complex branch-based workflows. You can read more about our versioning model [here](/content/concepts/versioning).
## How it works
In Mesa, you organize resources using a special kind of folder called a `repository`. Each repository gets its own version history and permissions policy.
A repository is a chain of `Changes`, or snapshots of the folder's contents at points in time. You can always go back and view the state of files at an older
`Change` and even restore the state of one or more files from that older `Change`.
In Mesa, reads and writes are durable automatically. All writes are automatically persisted to the `Change` checked out in your active
MesaFS instance.
See [Quickstart](/content/getting-started/quickstart) for more.
## Use cases
Mesa is designed for high-throughput, machine-driven workflows where you need reliable
versioning, low-latency reads and writes, and infinite scaling across many different repositories.
It works wherever your agents run, whether in a sandbox or in-process.
You can use it with a variety of tools and frameworks.
Some common use cases that Mesa is optimized for:
* Memory and skill management
* Prompt-to-app builders
* SWE agents
* Knowledge bases / company brains
* Agent infrastructure like sandboxes
## Benefits over traditional Git hosting
* APIs designed to be used by agents
* Architected for high-volume automated workflows
* Fine-grained access tokens for precise permissioning
* Virtual filesystem for easy agent access
## Pricing
See our [pricing page](https://mesa.dev/pricing) for more details.
## Up Next
Create your first repository and experiment with versioning.
Learn about Mesa's versioning model and how it maps to your use case.
Explore Mesa's virtual filesystem for easy reads and writes.
Learn the common patterns for building agent workflows on Mesa.
## FAQ
GitHub is built for human developers collaborating through pull requests.
Mesa is built for machines. Our APIs, virtual filesystem, and versioning model are designed for high-throughput automated workflows where thousands of ephemeral agents read and write concurrently.
Mesa also supports more ergonomic checkpoint-style versioning that doesn't require the ceremony of Git, making it easier to use in agentic applications.
Mesa offers official SDKs for [TypeScript](/content/reference/ts/index) and [Python](/content/reference/py/index). The REST API can be called from any language. Mesa works with agent frameworks like Vercel AI SDK, Langchain, and Mastra, and integrates with sandbox providers like [Daytona](/content/integrations/sandboxes/daytona) and [E2B](/content/integrations/sandboxes/e2b).
Changes are Mesa's core versioning primitive, similar to Git commits. Each change captures a snapshot of edits with a message and author. Bookmarks are lightweight pointers to changes, similar to Git branches. Together they support everything from simple linear checkpoints to full branching and merging workflows. See [Versioning](/content/concepts/versioning) for more.
The virtual filesystem supports **Linux** (via FUSE3), including both Debian/Ubuntu and Alpine distributions. For environments where a native mount isn't available, the TypeScript SDK includes an app mount that works anywhere Node.js runs. See [Filesystem](/content/concepts/filesystem) for details.
Yes. Attach a GitHub upstream to any Mesa repository, then trigger syncs via `syncUpstream` in the SDK or the upstream syncs REST API. See [GitHub](/content/integrations/github/sync) for setup and auth options.
Mesa supports custom on-prem deployments for enterprise customers. By default, Mesa runs as a hosted service at [app.mesa.dev](https://app.mesa.dev). If you're interested in on-prem deployment, reach out to us over email at [founders@mesa.dev](mailto:founders@mesa.dev).
# Quickstart
Source: https://docs.mesa.dev/content/getting-started/quickstart
Create a repository and write your first change.
In this quickstart, you will create a repo, read and write files with MesaFS, and experiment with versioning. The goal is to show the primitives Mesa provides that can be used for any of your agent workflows.
1. Create an account at [app.mesa.dev](https://app.mesa.dev).
2. Create an organization for your product (example: `acme`).
3. Open the organization's settings and create a key under **Keys**.
Store the key as an environment variable:
```bash theme={null}
export MESA_PRIVATE_KEY="mesa_private_key_acme_..."
```
The private key is shown once. Save it in your secrets manager before leaving the dashboard. Mesa stores only the public key.
Your SDK code uses the private key directly in trusted processes, while the CLI authenticates with a short-lived token minted from it. See [Authentication](/content/concepts/authentication) for the complete model.
```bash TypeScript theme={null}
npm install @mesadev/sdk
```
```bash Python theme={null}
pip install mesa-sdk
```
```bash CLI (Linux only) theme={null}
curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0
```
The CLI runs on Linux only, since MesaFS requires FUSE. If testing with the CLI we recommend
using a FUSE-enabled sandbox. See [Sandboxes](/content/integrations/sandboxes/daytona) for more.
In Mesa, you start by creating a special kind of folder called a `repository`. Each repository has its own version history and permissions.
Repositories are free to create. Use them liberally to isolate resources that belong to different customers or projects.
```typescript TypeScript theme={null}
import { Mesa } from "@mesadev/sdk";
const mesa = new Mesa({ privateKey: process.env.MESA_PRIVATE_KEY });
const repo = await mesa.repos.create({ name: "my-project" });
```
```python Python theme={null}
import os
from mesa_sdk import Mesa
mesa = Mesa(private_key=os.environ["MESA_PRIVATE_KEY"])
repo = await mesa.repos.create(name="my-project")
```
```bash CLI theme={null}
# Export a short-lived access token minted by an SDK client first,
# e.g. with mesa.fs({ layout, ttl }).token() in the TypeScript or Python SDK.
export MESA_ACCESS_TOKEN="eyJ..."
mesa repo create my-project
```
To retrieve an existing repo later:
```typescript TypeScript theme={null}
const repo = await mesa.repos.get({ repo: "my-project" });
```
```python Python theme={null}
repo = await mesa.repos.get(repo="my-project")
```
```bash CLI theme={null}
mesa repo list
```
Once created, the easiest way to read and write to a repository is by mounting MesaFS, either through our SDKs or through a FUSE mount with our CLI.
```typescript TypeScript theme={null}
import { Mesa, repo } from "@mesadev/sdk";
const mesa = new Mesa({ privateKey: process.env.MESA_PRIVATE_KEY });
const created = await mesa.repos.create({ name: "my-project" });
// Open the repository at a path you choose
const fs = await mesa.fs({
layout: {
"/workspace": repo(created.name, { mode: "rw", at: { bookmark: "main" } }),
},
authors: [{ name: "Quickstart", email: "quickstart@example.com" }],
}).mount();
// Use explicit filesystem operations
await fs.mkdir("/workspace/memories", { recursive: true });
await fs.writeFile("/workspace/memories/run-1.md", "Hello, world!");
const content = await fs.readFile("/workspace/memories/run-1.md", "utf8");
// Or use the emulated bash environment
const { stdout } = await fs.bash({ cwd: "/workspace" }).exec("echo memories/run-1.md");
```
```python Python theme={null}
import os
from mesa_sdk import Mesa, repo
mesa = Mesa(private_key=os.environ["MESA_PRIVATE_KEY"])
created = await mesa.repos.create(name="my-project")
# Open the repository at a path you choose
async with mesa.fs(
layout={"/workspace": repo(created.name, mode="rw", at={"bookmark": "main"})},
authors=[{"name": "Quickstart", "email": "quickstart@example.com"}],
).mount() as fs:
# Use explicit filesystem operations
await fs.mkdir("/workspace/memories", recursive=True)
await fs.write("/workspace/memories/run-1.md", b"Hello, world!")
content = await fs.read("/workspace/memories/run-1.md")
# Or use the emulated bash environment
result = await fs.bash(cwd="/workspace").exec("echo memories/run-1.md")
print(result.stdout.decode())
```
```bash CLI theme={null}
# Open the repository locally as a virtual filesystem
export MESA_ACCESS_TOKEN="eyJ..."
mesa repo create my-project
# A layout declares what the mount contains
cat > layout.json <<'EOF'
{ "/acme/my-project": { "kind": "repo", "name": "my-project", "mode": "rw" } }
EOF
mesa mount --layout layout.json --daemonize
# Use explicit filesystem operations
mkdir -p ~/.local/share/mesa/mnt/acme/my-project/memories
printf "Hello, world!" > ~/.local/share/mesa/mnt/acme/my-project/memories/run-1.md
cat ~/.local/share/mesa/mnt/acme/my-project/memories/run-1.md
# Or use the mounted directory with regular shell commands
cd ~/.local/share/mesa/mnt/acme/my-project
echo memories/run-1.md
```
A repository is a directed acyclic graph of `Changes`: snapshots of the repository at that point in time. See more in [Versioning](/content/concepts/versioning).
Every file write is part of specific change. Your first change is created automatically
when you mount an empty repo in MesaFS. All subsequent changes must be explicitly created.
```typescript TypeScript theme={null}
// The first Change that holds our previous writes
const firstChange = await fs.change.current({ repo: "my-project" });
// Create a new Change on top of the first
await fs.change.new({ repo: "my-project", changeId: firstChange.changeId });
// All writes go to the new change
await fs.bash({ cwd: "/workspace" }).exec('echo "Hello, Mesa!" > memories/run-1.md');
const secondChange = await fs.change.current({ repo: "my-project" });
```
```python Python theme={null}
# The first Change that holds our previous writes
first_change = await fs.changes.current("my-project")
# Create a new Change on top of the first
await fs.changes.new("my-project", change_id=first_change.change_id)
# All writes go to the new change
await fs.bash(cwd="/workspace").exec('echo "Hello, Mesa!" > memories/run-1.md')
second_change = await fs.changes.current("my-project")
```
```bash CLI theme={null}
# Create a new Change on top of main
cd ~/.local/share/mesa/mnt/acme/my-project
mesa new main
# All writes go to the new change
echo "Hello, Mesa!" > memories/run-1.md
# Inspect the new change
mesa show --summary
```
You can easily roll back to a previous version by switching to an old Change.
```typescript TypeScript theme={null}
// Roll back to the first change
await fs.change.edit({ repo: "my-project", changeId: firstChange.changeId });
const original = await fs.readFile("/workspace/memories/run-1.md", "utf8");
console.log("Content:", original); // "Hello, world!"
// Continue with the new change
await fs.change.edit({ repo: "my-project", changeId: secondChange.changeId });
const updated = await fs.readFile("/workspace/memories/run-1.md", "utf8");
console.log("Content:", updated); // "Hello, Mesa!"
```
```python Python theme={null}
# Roll back to the first change
await fs.changes.edit("my-project", change_id=first_change.change_id)
original = await fs.read("/workspace/memories/run-1.md")
print("Content:", original.decode()) # "Hello, world!"
# Continue with the new change
await fs.changes.edit("my-project", change_id=second_change.change_id)
updated = await fs.read("/workspace/memories/run-1.md")
print("Content:", updated.decode()) # "Hello, Mesa!"
```
```bash CLI theme={null}
# Roll back to the first change
mesa edit "$FIRST_CHANGE" --repo acme/my-project
cat ~/.local/share/mesa/mnt/acme/my-project/memories/run-1.md
# Continue with the new change
mesa edit "$SECOND_CHANGE" --repo acme/my-project
cat ~/.local/share/mesa/mnt/acme/my-project/memories/run-1.md
```
Now you have two changes in your repo.
You can imagine having many different changes, each representing the concurrent work of a different agent. Once an agent is done working,
you may want to show a UI for users to review the changes and approve or reject them.
```typescript TypeScript theme={null}
const diffResult = await mesa.diffs.get({
repo: "my-project",
base_change_id: firstChange.changeId,
head_change_id: secondChange.changeId,
});
diffResult.entries.forEach((entry) => {
// render file diff
});
```
```python Python theme={null}
diff_result = await mesa.diffs.get(
repo="my-project",
base_change_id=first_change.change_id,
head_change_id=second_change.change_id,
)
for entry in diff_result.entries:
# render file diff
pass
```
```bash CLI theme={null}
# Show the diff for the second change
mesa show "$SECOND_CHANGE" --repo acme/my-project --git
```
By default, changes have random, alphanumeric identifiers. However, you will often want to assign a human-readable name to a change. You can do this with bookmarks.
By default, the first change on a repo is bookmarked `main`.
```typescript TypeScript theme={null}
await mesa.bookmarks.create({
repo: "my-project",
name: "my-feature",
change_id: secondChange.changeId,
});
```
```python Python theme={null}
await mesa.bookmarks.create(
repo="my-project",
name="my-feature",
change_id=second_change.change_id,
)
```
```bash CLI theme={null}
mesa bookmark create my-feature --repo acme/my-project --revision "$SECOND_CHANGE"
```
You will eventually have lots of different changes and bookmarks and you'll want to merge the changes from one bookmark into another.
Merge your source bookmark (`my-feature`) into your target bookmark (`main`). This process creates a new Change on top of
`main` that contains the merged changes and moves the `main` bookmark to the new Change.
```typescript TypeScript theme={null}
const mergedChange = await mesa.bookmarks.merge({
repo: "my-project",
target: "main",
source: "my-feature",
authors: [{ name: "Quickstart", email: "quickstart@example.com" }],
});
console.log("Merged Change:", mergedChange.change_id);
```
```python Python theme={null}
merged_change = await mesa.bookmarks.merge(
repo="my-project",
target="main",
source="my-feature",
authors=[{"name": "Quickstart", "email": "quickstart@example.com"}],
)
print("Merged Change:", merged_change.change_id)
```
Now you have 3 changes in your repo.
Conventionally, the `main` bookmark points to the canonical
version of your documents, and other bookmarks represent "draft" work. A typical pattern is rendering diff UI for human-in-the-loop
approvals, then merging approved changes into your `main` line of Change history.
You're now ready to start building complex agent workflows with Mesa.
## Next steps
* [Versioning](/content/concepts/versioning) for repos, bookmarks, and changes
* [Authentication](/content/concepts/authentication) for private keys, access tokens, and compatibility
* [TypeScript SDK Reference](/content/reference/ts/index) for the primary TypeScript client
* [Python SDK Reference](/content/reference/py/index) if you're working in Python
# GitHub
Source: https://docs.mesa.dev/content/integrations/github/sync
Sync repositories between Mesa and GitHub (or any Git server).
Attach an upstream Git repository to a Mesa repository and trigger syncs with `syncUpstream`. Syncs report their progress through `sync.*` webhook events and are exposed on the repository's `upstream` field.
The upstream is per-repository configuration: one Mesa repo points at one upstream URL, with optional stored credentials. The upstream can be GitHub, GitLab, Bitbucket, or any Git server that supports the Git HTTPS smart protocol.
Sync leaves Git attribution alone. Mesa reads the commit's own author along with any valid
`Co-authored-by` trailers at the end of the message, and it never rewrites the commit or changes
its OID. Commits that Mesa itself created with several authors already carry those trailers, so
attribution survives a push to GitHub and a later pull back into Mesa.
## Sync semantics
By default, Mesa's GitHub sync is conservative, and will only update branches and tags that have not diverged (fast-forward only). Deletions are not synced. This behavior is the same both when pulling upstream changes and when pushing Mesa changes to an upstream.
Mesa attempts to sync all branches and tags by default. You can also filter branches and tags for a single sync with glob patterns. If some matching branches and tags cannot be safely updated (non-fast-forward, branch protection rules, etc.), those updates will be skipped without failing the entire sync. Once a sync completes, per-ref sync outcomes are surfaced in the sync's `stats` field.
## Configure an upstream
Set an upstream when creating the repository, or attach one later with `mesa.repos.update(...)`. The `auth` payload is **write-only** — Mesa stores credentials securely and only returns the `auth_kind` on read. Repository responses include `upstream.latest_sync`, which is `null` until the first sync is enqueued.
```typescript TypeScript theme={null}
import { Mesa } from "@mesadev/sdk";
const mesa = new Mesa({ privateKey: process.env.MESA_PRIVATE_KEY });
await mesa.repos.update({
repo: "app",
upstream: {
url: "https://github.com/acme/app.git",
auth: {
kind: "token",
token: process.env.GITHUB_PAT!,
token_username: "x-access-token",
},
},
});
```
```python Python theme={null}
import os
from mesa_sdk import Mesa
from mesa_sdk.types import TokenAuth, UpstreamConfig
mesa = Mesa(private_key=os.environ["MESA_PRIVATE_KEY"])
await mesa.repos.update(
repo="app",
upstream=UpstreamConfig(
url="https://github.com/acme/app.git",
auth=TokenAuth(
token=os.environ["GITHUB_PAT"],
token_username="x-access-token",
),
),
)
```
## Auth options
| `auth.kind` | When to use |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `token` | A GitHub personal access token (`github_pat_...`) or other single-token credential. `token_username` is optional; most Git servers will accept basic auth with an arbitrary username. One notable exception is BitBucket, which expects the token owner's username. |
| `username_password` | Classic username/password basic auth. Use for self-hosted Git servers or providers that do not issue tokens. |
| *(omitted)* | Public upstream. Reads work without auth; pushes will fail. |
On `repos.update(...)`, omitting a value for upstream `auth` preserves the stored credential, while passing `null`/`None` clears the stored credential:
| Value | Behavior |
| ----------------------------------------------------------- | --------------------------------------------------------- |
| Omitted | Preserve the stored credential. |
| `null` (TS) / `None` (Python) | Clear the stored credential. The upstream becomes public. |
| `{ kind: ..., ... }` / `TokenAuth` / `UsernamePasswordAuth` | Replace the stored credential. |
## Trigger a sync
`syncUpstream({ direction: "pull" })` fetches branches and tags from the upstream repository and applies any safe updates to the Mesa repository. `direction: "push"` works the same way in the other direction; branches and tags are fetched from your Mesa repository, and any safe updates are applied to the upstream repo.
Omit `ref_globs` or use `{ branches: "*", tags: "*" }` to sync all supported branches and tags. To sync only selected refs, pass branch and tag glob filters. If you provide `ref_globs`, include at least one of `branches` or `tags`; an omitted namespace matches no refs. Branches and tags are plain names such as `main`, `release/*`, or `v1.*`, not fully-qualified Git refs such as `refs/heads/main`.
This method enqueues a `sync` job and returns an object containing the id of the queued sync. The work is performed asynchronously. Fetch the repository and read `repo.upstream.latest_sync`, fetch the sync by id, or list the repository's upstream sync history to check status.
```typescript TypeScript theme={null}
// Pull upstream changes into Mesa
const sync = await mesa.repos.syncUpstream({ repo: "app", direction: "pull" });
console.log(sync.id, sync.status); // "sync_...", "queued"
// Or pull only matching branches and tags
await mesa.repos.syncUpstream({
repo: "app",
direction: "pull",
ref_globs: {
branches: "main",
},
});
// Or push Mesa changes to the upstream
await mesa.repos.syncUpstream({ repo: "app", direction: "push" });
```
```python Python theme={null}
# Pull upstream changes into Mesa
sync = await mesa.repos.sync_upstream(repo="app", direction="pull")
print(sync.id, sync.status) # "sync_...", "queued"
# Or pull only matching branches and tags
await mesa.repos.sync_upstream(
repo="app",
direction="pull",
ref_globs={
"branches": "main",
},
)
# Or push Mesa changes to the upstream
await mesa.repos.sync_upstream(repo="app", direction="push")
```
```typescript TypeScript theme={null}
// See the latest sync status on the repo object
const repo = await mesa.repos.get({ repo: "app" });
console.log(repo.upstream?.latest_sync?.status, repo.upstream?.latest_sync?.stats);
// Or fetch the status of a specific sync
const sync = await mesa.repos.getUpstreamSync({ repo: "app", syncId: "sync_..." });
console.log(sync.status, sync.stats);
// Or list the sync history for a repo
const { syncs } = await mesa.repos.listUpstreamSyncs({ repo: "app", limit: 20 });
for (const sync of syncs) {
console.log(sync.direction, sync.status, sync.created_at);
}
```
```python Python theme={null}
# See the latest sync status on the repo object
repo = await mesa.repos.get(repo="app")
print(repo.upstream.latest_sync.status if repo.upstream and repo.upstream.latest_sync else None)
# Or fetch the status of a specific sync
sync = await mesa.repos.get_upstream_sync(repo="app", sync_id="sync_...")
print(sync.status, sync.stats)
# Or list the sync history for a repo
result = await mesa.repos.list_upstream_syncs(repo="app", limit=20)
for sync in result.syncs:
print(sync.direction, sync.status, sync.created_at)
```
`status` transitions through `queued` → `in_progress` → `completed` or `failed`.
Once a sync completes, a `stats` field becomes available with per-ref outcomes (`updated`, `unchanged`, `filtered`, or `rejected`). `stats.refs` includes only matching source branches and tags.
If a sync fails, the `error` field provides a failure reason.
## Subscribe to sync events
Syncs emit four webhook event types over their lifecycle. Subscribe on a webhook target to drive your own workflows without polling.
| Event | Fires when |
| ------------------ | ------------------------------------------------------------------------------ |
| `sync.queued` | A `syncUpstream` call enqueues a new sync. |
| `sync.in_progress` | The worker picks up the sync. |
| `sync.completed` | The sync finishes with terminal status `completed`. |
| `sync.failed` | The sync finishes with terminal status `failed`. The payload includes `error`. |
```ts theme={null}
const target = await mesa.webhookTargets.create({
url: "https://acme.dev/webhooks/mesa",
events: ["sync.completed", "sync.failed"],
repo_ids: ["repo_abc123"],
});
```
Pull syncs also cause the `repo.push` webhook event to fire if upstream changes are received by Mesa.
See [Webhooks](/content/concepts/webhooks) for delivery semantics, signature verification, and the full event payload shape.
## Required scopes
| Operation | Scope |
| -------------------------------- | ------------------------- |
| Attach or update upstream config | `write` on the repository |
| Sync upstream | `write` on the repository |
| Read sync history | `read` on the repository |
## REST endpoints
Each SDK method maps to a REST endpoint under the organization root:
| Method | Endpoint |
| ------------------- | ------------------------------------------------ |
| Configure upstream | `POST /v1/:org/repos` and `PATCH /v1/:org/:repo` |
| Sync upstream | `POST /v1/:org/:repo/upstream/syncs` |
| Get upstream sync | `GET /v1/:org/:repo/upstream/syncs/:syncId` |
| List upstream syncs | `GET /v1/:org/:repo/upstream/syncs` |
See the [REST API reference](/content/api-reference/overview) for full request and response schemas.
# Blaxel
Source: https://docs.mesa.dev/content/integrations/sandboxes/blaxel
Use Mesa with Blaxel sandboxes for secure agent workflows.
[Blaxel](https://blaxel.ai/) provides lightweight sandboxes that work well with Mesa. This guide shows the full end-to-end flow: set up Mesa inside a Blaxel sandbox and mount your repos as local directories.
The general flow for any sandbox integration is:
1. **Outside the sandbox** — use the Mesa SDK (TypeScript or Python) to create repos, sign a short-lived access token with your private key, and orchestrate your workflow.
2. **Inside the sandbox** — install the `mesa` CLI and run `mesa mount --daemonize` with the token in `MESA_ACCESS_TOKEN`.
3. **Run your agent** — `cd` into the mount path and launch your agent (e.g. Claude Code, Codex, or a custom agent). Any file edits are automatically persisted back to Mesa.
For details on FUSE setup, system dependencies, and container configuration, see [POSIX Mount](/content/mesafs/posix-mount).
## Alpine-based setup (default image)
Blaxel's default image is Alpine-based. The Mesa install script handles Alpine natively — it detects the architecture, adds the correct APK repository, and installs Mesa:
```typescript TypeScript theme={null}
import { SandboxInstance } from "@blaxel/core";
const sandbox = await SandboxInstance.create({ region: "us-pdx-1" });
// Install system dependencies and Mesa.
// gcompat (not libc6-compat) is required — the Mesa daemon's gRPC
// connections deadlock under libc6-compat's musl shim.
await sandbox.process.exec({
command: "apk add --no-cache curl ca-certificates gcompat fuse3 && curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0",
waitForCompletion: true,
});
```
```python Python theme={null}
# Install system dependencies and Mesa.
# gcompat (not libc6-compat) is required — the Mesa daemon's gRPC
# connections deadlock under libc6-compat's musl shim.
# Run this command with Blaxel's Python sandbox command runner.
install_cmd = "apk add --no-cache curl ca-certificates gcompat fuse3 && curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0"
```
```bash CLI theme={null}
# Install system dependencies and Mesa.
# gcompat (not libc6-compat) is required — the Mesa daemon's gRPC
# connections deadlock under libc6-compat's musl shim.
apk add --no-cache curl ca-certificates gcompat fuse3
curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0
```
The layout scopes the token: it can reach the repositories the layout declares
and nothing else. A plain `mesa mount` then shows exactly those repositories
under the organization browse tree. To mount the layout's own paths instead,
serialize `workspace.layout()` with `JSON.stringify(...)` in TypeScript or
`json.dumps(...)` in Python, write it into the sandbox, and mount with
`mesa mount --layout `.
## Debian-based setup (custom template)
If you prefer Debian, you can build a custom Blaxel template with Mesa pre-installed. Create a Dockerfile:
```dockerfile theme={null}
FROM debian:bookworm-slim
COPY --from=ghcr.io/blaxel-ai/sandbox:latest /sandbox-api /usr/local/bin/sandbox-api
RUN apt-get update && apt-get install -y curl fuse3 ca-certificates \
&& curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0 \
&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["/usr/local/bin/sandbox-api"]
```
Deploy with `bl deploy`, then reference your template when creating sandboxes:
```typescript TypeScript theme={null}
const sandbox = await SandboxInstance.create({
image: "your-template:latest",
region: "us-pdx-1",
});
```
```python Python theme={null}
# Use the same image and region with Blaxel's Python SDK.
sandbox_options = {
"image": "your-template:latest",
"region": "us-pdx-1",
}
```
## Mount Mesa
Once the CLI is installed (via either method), configure and mount:
```typescript TypeScript theme={null}
import { Mesa, repo } from "@mesadev/sdk";
const mesa = new Mesa({ privateKey: process.env.MESA_PRIVATE_KEY });
// Create a repo (or use an existing one)
const created = await mesa.repos.create({ name: "agent-workspace" });
// Sign a scoped, self-expiring access token for the sandbox. Signed locally
// with your private key, no network call. The private key never enters
// the sandbox.
const workspace = mesa.fs({
layout: { "/workspace": repo(created.name, { mode: "rw" }) },
authors: [{ name: "Sandbox Agent", email: "agent@example.com" }],
ttl: 60 * 60, // 1 hour, max 4 hours
});
const { token } = await workspace.token();
// Start Mesa as a background daemon with the short-lived token.
await sandbox.process.exec({
command: `MESA_ACCESS_TOKEN=${token} mesa mount --daemonize`,
waitForCompletion: true,
});
// Your repos are now at ~/.local/share/mesa/mnt//
await sandbox.process.exec({
command: `cd ~/.local/share/mesa/mnt/${created.org}/${created.name} \
&& claude -p "Implement the feature described in TODO.md"`,
waitForCompletion: true,
});
```
```python Python theme={null}
import os
from mesa_sdk import Mesa, repo
mesa = Mesa(private_key=os.environ["MESA_PRIVATE_KEY"])
# Create a repo (or use an existing one)
created = await mesa.repos.create(name="agent-workspace")
# Sign a scoped, self-expiring access token for the sandbox. Signed locally
# with your private key, no network call. The private key never enters
# the sandbox.
workspace = mesa.fs(
layout={"/workspace": repo(created.name, mode="rw")},
authors=[{"name": "Sandbox Agent", "email": "agent@example.com"}],
ttl=60 * 60, # 1 hour, max 4 hours
)
minted = await workspace.token()
# Start Mesa as a background daemon with the short-lived token.
# Run this command with Blaxel's Python sandbox command runner.
mount_cmd = f"MESA_ACCESS_TOKEN={minted.token} mesa mount --daemonize"
# Your repos are now at ~/.local/share/mesa/mnt//
# Run this command with Blaxel's Python sandbox command runner.
agent_cmd = f'cd ~/.local/share/mesa/mnt/{created.org}/{created.name} && claude -p "Implement the feature described in TODO.md"'
```
```bash CLI theme={null}
# Start Mesa as a background daemon.
MESA_ACCESS_TOKEN="$MESA_ACCESS_TOKEN" mesa mount --daemonize
# Your repos are now at ~/.local/share/mesa/mnt//
cd ~/.local/share/mesa/mnt/my-org/agent-workspace
claude -p "Implement the feature described in TODO.md"
```
An access token is minted once with a fixed TTL and is never refreshed: there is no
background rotation and no access-token replacement. Tokens default to a 15 minute TTL and max out at 4 hours, so
mint one whose TTL covers the whole agent session. After it expires, filesystem operations in the sandbox fail
with authentication errors. To continue past expiry, mint a fresh token on the host (the private key lives only
outside the sandbox) and remount inside the sandbox with the new token in `MESA_ACCESS_TOKEN`.
## Tips
* **Use scoped, short-lived access tokens.** Sign a dedicated token for each sandbox session with only the scopes it needs — it's signed locally with your private key (which never enters the sandbox) and expires on its own. See [Authentication](/content/concepts/authentication) for details.
* **Use `--daemonize`.** Always run `mesa mount --daemonize` in sandbox environments so Mesa runs as a background process and doesn't block your agent's terminal.
* **Alpine works out of the box.** The install script handles Alpine natively, so the default Blaxel image works without workarounds. Use a custom Debian template only if you have other reasons to prefer Debian.
# Cloudflare
Source: https://docs.mesa.dev/content/integrations/sandboxes/cloudflare
Use Mesa with Cloudflare Workers and containers.
This page is a draft and may be incomplete or change significantly. If you have questions, reach out to us on [Discord](https://discord.gg/mesa).
Coming soon.
# Daytona
Source: https://docs.mesa.dev/content/integrations/sandboxes/daytona
Use Mesa with Daytona sandboxes for secure, high-performance agent workflows.
[Daytona](https://daytona.io/) provides secure, high-performance sandboxes that work well with Mesa. This guide shows the full end-to-end flow: use the Mesa SDK outside the sandbox to set up resources and sign a short-lived access token, then use the Daytona SDK to inject that token and mount Mesa inside the sandbox.
The general flow for any sandbox integration is:
1. **Outside the sandbox** — the orchestrator holds the private key. Use the Mesa SDK (TypeScript or Python) to create repos, then sign a short-lived access token locally with `mesa.fs({ layout, ttl }).token()`. The layout scopes the token to the repositories it declares. Only the token crosses into the sandbox; the private key never does.
2. **Inside the sandbox** — install the `mesa` CLI and run `mesa mount --daemonize` with the token in `MESA_ACCESS_TOKEN`.
3. **Run your agent** — `cd` into the mount path and launch your agent (e.g. Claude Code, Codex, or a custom agent). Any file edits are automatically persisted back to Mesa.
For details on FUSE setup, system dependencies, and container configuration, see [POSIX Mount](/content/mesafs/posix-mount).
## Image setup
First, ensure that your Daytona image is properly configured. This example uses Daytona's [declarative image builder](https://www.daytona.io/docs/en/declarative-builder/) to install Mesa and configure FUSE in the image.
```typescript TypeScript theme={null}
import { Image } from "@daytona/sdk";
// Define a declarative image with Mesa dependencies
const mesaImage = Image.base("ubuntu:24.04").runCommands(
// Install Mesa required system dependencies
"apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && rm -rf /var/lib/apt/lists/*",
"curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0 --yes",
// Enable user_allow_other in FUSE config. This is required for non-root
// users to access the mounted filesystem.
"sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf",
);
```
```python Python theme={null}
from daytona import Image
mesa_image = Image.base("ubuntu:24.04").run_commands(
"apt-get update && apt-get install -y --no-install-recommends "
"ca-certificates curl && rm -rf /var/lib/apt/lists/*",
"curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0 --yes",
# Enable user_allow_other in FUSE config. This is required for non-root
# users to access the mounted filesystem.
"sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf",
)
```
```dockerfile Dockerfile theme={null}
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0 --yes
RUN sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf
```
The layout scopes the token: it can reach the repositories the layout declares
and nothing else. A plain `mesa mount` then shows exactly those repositories
under the organization browse tree. To mount the layout's own paths instead,
serialize `workspace.layout()` with `JSON.stringify(...)` in TypeScript or
`json.dumps(...)` in Python, write it into the sandbox, and mount with
`mesa mount --layout `.
The image installs the latest Mesa CLI when Daytona builds it. Rebuild the image to pick up a newer CLI.
## Create and mount
The following examples create a temporary repo, mount it in a Daytona sandbox, write and read a file, and then delete both resources.
```typescript TypeScript theme={null}
import { Daytona } from "@daytona/sdk";
import { Mesa, repo } from "@mesadev/sdk";
const mesa = new Mesa({ privateKey: process.env.MESA_PRIVATE_KEY! });
const daytona = new Daytona();
const sandbox = await daytona.create(
{
image: mesaImage,
ephemeral: true,
ttlMinutes: 30, // 30 minutes
},
{
timeout: 10 * 60, // 10 minutes
},
);
let created: { name: string; org: string } | undefined;
try {
repo = await mesa.repos.create({ name: `daytona-${Date.now()}` });
// Sign a self-expiring access token for the sandbox. This is signed locally
// with your private key and no network call. Tokens are not stored anywhere:
// when the TTL elapses, the token is dead. Nothing to revoke.
// Pick a TTL that covers your agent session (max 4 hours).
const workspace = mesa.fs({
layout: { "/workspace": repo(created.name, { mode: "rw" }) },
authors: [{ name: "Sandbox Agent", email: "agent@example.com" }],
ttl: 30 * 60, // 30 minutes
});
const { token } = await workspace.token();
// Pass the token only to the mount command. The private key never enters the
// sandbox, and the token is never persisted to disk.
// By default, MesaFS mounts every repo the token can access. Since this token
// is scoped to the temporary repo, that is the only repo in the mount.
const mount = await sandbox.process.executeCommand("mesa mount --daemonize", undefined, {
MESA_ACCESS_TOKEN: token,
});
if (mount.exitCode !== 0) throw new Error(mount.result);
const home = await sandbox.getUserHomeDir();
const repoPath = `${home}/.local/share/mesa/mnt/${created.org}/${created.name}`;
const result = await sandbox.process.executeCommand(
"printf 'Hello from Daytona and Mesa!\\n' > hello-from-daytona.txt && cat hello-from-daytona.txt",
repoPath,
);
if (result.exitCode !== 0) throw new Error(result.result);
console.log(result.result);
} finally {
try {
await sandbox.delete();
} finally {
if (created) await mesa.repos.delete({ repo: created.name });
}
}
```
```python Python theme={null}
import asyncio
import os
import time
from daytona import CreateSandboxFromImageParams, Daytona
from mesa_sdk import Mesa, repo
async def main():
async with Mesa(private_key=os.environ["MESA_PRIVATE_KEY"]) as mesa:
daytona = Daytona()
sandbox = daytona.create(
CreateSandboxFromImageParams(
image=mesa_image,
ephemeral=True,
ttl_minutes=30, # 30 minutes
),
timeout=10 * 60, # 10 minutes
)
repo = None
try:
created = await mesa.repos.create(
name=f"daytona-{int(time.time() * 1000)}"
)
# Sign a self-expiring access token for the sandbox. This is signed
# locally with your private key and no network call. Tokens are not
# stored anywhere: when the TTL elapses, the token is dead. Nothing
# to revoke. Pick a TTL that covers your agent session (max 4 hours).
workspace = mesa.fs(
layout={"/workspace": repo(created.name, mode="rw")},
authors=[{"name": "Sandbox Agent", "email": "agent@example.com"}],
ttl=30 * 60, # 30 minutes
)
token = await workspace.token()
# Pass the token only to the mount command. The private key never enters
# the sandbox, and the token is never persisted to disk.
# By default, MesaFS mounts every repo the token can access. Since this
# token is scoped to the temporary repo, that is the only repo in the mount.
mount = sandbox.process.exec(
"mesa mount --daemonize",
env={"MESA_ACCESS_TOKEN": token.token},
)
if mount.exit_code != 0:
raise RuntimeError(mount.result)
home = sandbox.get_user_home_dir()
repo_path = f"{home}/.local/share/mesa/mnt/{created.org}/{created.name}"
result = sandbox.process.exec(
"printf 'Hello from Daytona and Mesa!\\n' > hello-from-daytona.txt "
"&& cat hello-from-daytona.txt",
cwd=repo_path,
)
if result.exit_code != 0:
raise RuntimeError(result.result)
print(result.result)
finally:
try:
sandbox.delete()
finally:
if repo is not None:
await mesa.repos.delete(repo=created.name)
asyncio.run(main())
```
```bash CLI theme={null}
# By default, MesaFS mounts every repo allowed by MESA_ACCESS_TOKEN. Scope the
# token to the repos this sandbox should be able to access.
mesa mount --daemonize
cd "$HOME/.local/share/mesa/mnt/my-org"/daytona-*
printf 'Hello from Daytona and Mesa!\n' > hello-from-daytona.txt
```
For runnable versions that open an interactive shell in the mounted repo, see the [TypeScript](https://github.com/mesa-dot-dev/examples/tree/main/daytona-shell) and [Python](https://github.com/mesa-dot-dev/examples/tree/main/daytona-python-shell) examples.
Daytona's Secrets API only substitutes placeholders in HTTPS request headers. MesaFS authenticates over gRPC (HTTP/2), which the substitution proxy doesn't handle, so a token passed as a Secret never reaches the mount. Inject `MESA_ACCESS_TOKEN` as a plain environment variable instead.
An access token is minted once with a fixed TTL and is never refreshed: there is no
background rotation and no access-token replacement. Tokens default to a 15 minute TTL and max out at 4 hours, so
mint one whose TTL covers the whole agent session. After it expires, filesystem operations in the sandbox fail
with authentication errors. To continue past expiry, mint a fresh token on the host (the private key lives only
outside the sandbox) and remount inside the sandbox with the new token in `MESA_ACCESS_TOKEN`.
## Tips
* **Use access tokens, not the private key, inside sandboxes.** Tokens expire on their own, can't be used to sign further access tokens, and leave nothing behind to clean up. See [Authentication](/content/concepts/authentication) for details.
* **Use `--daemonize`.** Always run `mesa mount --daemonize` in sandbox environments so Mesa runs as a background process and doesn't block your agent's terminal.
* **Don't forget `user_allow_other`.** This is the most common setup issue in sandbox environments. See [POSIX Mount](/content/mesafs/posix-mount#allow_other-and-user_allow_other) for more info.
# E2B
Source: https://docs.mesa.dev/content/integrations/sandboxes/e2b
Use Mesa with E2B sandboxes for secure, high-performance agent workflows.
[E2B](https://e2b.dev/) provides open-source, secure cloud sandboxes for AI agents. This guide shows the full end-to-end flow: use the Mesa SDK outside the sandbox to set up resources, then use the E2B SDK to configure and mount Mesa inside the sandbox.
The general flow for any sandbox integration is:
1. **Outside the sandbox** — use the Mesa SDK (TypeScript or Python) to create repos, sign a short-lived access token with your private key, and orchestrate your workflow.
2. **Inside the sandbox** — install the `mesa` CLI and run `mesa mount --daemonize` with the token in `MESA_ACCESS_TOKEN`.
3. **Run your agent** — `cd` into the mount path and launch your agent (e.g. Claude Code, Codex, or a custom agent). Any file edits are automatically persisted back to Mesa.
For details on FUSE setup, system dependencies, and container configuration, see [POSIX Mount](/content/mesafs/posix-mount).
## Sandbox setup
E2B sandboxes are Debian-based by default, so the standard Mesa install script works out of the box. Two E2B specifics need root before mounting: `/dev/fuse` is exposed as root-only, so `chmod 666` it, and `user_allow_other` must be enabled in `/etc/fuse.conf` so non-root processes can access the mount. You can also build a [custom sandbox template](https://e2b.dev/docs/sandbox-template) with Mesa pre-installed to skip the install step at runtime.
```typescript TypeScript theme={null}
import { Sandbox } from "e2b";
import { Mesa, repo } from "@mesadev/sdk";
const mesa = new Mesa({ privateKey: process.env.MESA_PRIVATE_KEY });
// --- Outside the sandbox: set up Mesa resources ---
// Create a repo (or use an existing one)
const created = await mesa.repos.create({ name: "agent-workspace" });
// Sign a scoped, self-expiring access token for the sandbox. Signed locally
// with your private key, no network call. The private key never enters
// the sandbox.
const workspace = mesa.fs({
layout: { "/workspace": repo(created.name, { mode: "rw" }) },
authors: [{ name: "Sandbox Agent", email: "agent@example.com" }],
ttl: 60 * 60, // 1 hour, max 4 hours
});
const { token } = await workspace.token();
// --- Inside the sandbox: install and mount Mesa ---
const sandbox = await Sandbox.create();
// Install Mesa dependencies and the CLI.
// E2B exposes /dev/fuse as root-only by default, so we also fix permissions.
await sandbox.commands.run(
[
"apt-get update",
"apt-get install -y --no-install-recommends ca-certificates curl fuse3 gpg",
"sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf",
"chmod 666 /dev/fuse",
"curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0",
].join(" && "),
{ user: "root" }
);
// Start Mesa as a background daemon with the short-lived token. The private
// key never enters the sandbox.
await sandbox.commands.run("mesa mount --daemonize", {
envs: {
MESA_ACCESS_TOKEN: token,
},
});
// --- Run your agent ---
await sandbox.commands.run(
`cd ~/.local/share/mesa/mnt/${created.org}/${created.name} \
&& claude -p "Implement the feature described in TODO.md"`
);
```
```python Python theme={null}
import os
from e2b import Sandbox
from mesa_sdk import Mesa, repo
# --- Outside the sandbox: set up Mesa resources ---
mesa = Mesa(private_key=os.environ["MESA_PRIVATE_KEY"])
# Create a repo (or use an existing one)
created = await mesa.repos.create(name="agent-workspace")
# Sign a scoped, self-expiring access token for the sandbox. Signed locally
# with your private key, no network call. The private key never enters
# the sandbox.
workspace = mesa.fs(
layout={"/workspace": repo(created.name, mode="rw")},
authors=[{"name": "Sandbox Agent", "email": "agent@example.com"}],
ttl=60 * 60, # 1 hour, max 4 hours
)
minted = await workspace.token()
# --- Inside the sandbox: install and mount Mesa ---
sandbox = Sandbox()
# Install Mesa dependencies and the CLI.
# E2B exposes /dev/fuse as root-only by default, so we also fix permissions.
sandbox.commands.run(
" && ".join([
"apt-get update",
"apt-get install -y --no-install-recommends ca-certificates curl fuse3 gpg",
"sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf",
"chmod 666 /dev/fuse",
"curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0",
]),
user="root",
)
# Start Mesa as a background daemon with the short-lived token. The private
# key never enters the sandbox.
sandbox.commands.run(
"mesa mount --daemonize",
envs={"MESA_ACCESS_TOKEN": minted.token},
)
# --- Run your agent ---
sandbox.commands.run(
f'cd ~/.local/share/mesa/mnt/{created.org}/{created.name} && claude -p "Implement the feature described in TODO.md"'
)
```
```bash CLI theme={null}
# Install Mesa dependencies and the CLI. Run this block as root:
# E2B exposes /dev/fuse as root-only by default, so we also fix permissions.
apt-get update
apt-get install -y --no-install-recommends ca-certificates curl fuse3 gpg
sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf
chmod 666 /dev/fuse
curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0
# Start Mesa as a background daemon.
# Pass the short-lived token to the mount process.
MESA_ACCESS_TOKEN="$MESA_ACCESS_TOKEN" mesa mount --daemonize
# --- Run your agent ---
cd ~/.local/share/mesa/mnt/my-org/agent-workspace
claude -p "Implement the feature described in TODO.md"
```
The layout scopes the token: it can reach the repositories the layout declares
and nothing else. A plain `mesa mount` then shows exactly those repositories
under the organization browse tree. To mount the layout's own paths instead,
serialize `workspace.layout()` with `JSON.stringify(...)` in TypeScript or
`json.dumps(...)` in Python, write it into the sandbox, and mount with
`mesa mount --layout `.
The Mesa CLI version is pinned so sandbox setups are reproducible. Update the pinned version periodically to pick up fixes and new features.
An access token is minted once with a fixed TTL and is never refreshed: there is no
background rotation and no access-token replacement. Tokens default to a 15 minute TTL and max out at 4 hours, so
mint one whose TTL covers the whole agent session. After it expires, filesystem operations in the sandbox fail
with authentication errors. To continue past expiry, mint a fresh token on the host (the private key lives only
outside the sandbox) and remount inside the sandbox with the new token in `MESA_ACCESS_TOKEN`.
## Custom sandbox template
For faster startup, pre-install Mesa into a [custom E2B template](https://e2b.dev/docs/sandbox-template). Create a Dockerfile:
```dockerfile theme={null}
FROM e2b/base:latest
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl fuse3 gpg \
&& curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0 \
&& sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf \
&& chmod 666 /dev/fuse \
&& rm -rf /var/lib/apt/lists/*
```
Build and deploy with `e2b template build`, then reference your template when creating sandboxes:
```typescript TypeScript theme={null}
const sandbox = await Sandbox.create({ template: "my-mesa-template" });
```
```python Python theme={null}
from e2b import Sandbox
sandbox = Sandbox(template="my-mesa-template")
```
## Tips
* **Use scoped, short-lived access tokens.** Sign a dedicated token for each sandbox session with only the scopes and repos it needs. It expires on its own and can't mint further access tokens. See [Authentication](/content/concepts/authentication) for details.
* **Use `--daemonize`.** Always run `mesa mount --daemonize` in sandbox environments so Mesa runs as a background process and doesn't block your agent's terminal.
* **Don't forget `user_allow_other`.** This is the most common setup issue in sandbox environments. See [POSIX Mount](/content/mesafs/posix-mount#allow_other-and-user_allow_other) for more info.
* **Build a custom template** for production use. Pre-installing Mesa avoids the install overhead on every sandbox creation.
# Freestyle
Source: https://docs.mesa.dev/content/integrations/sandboxes/freestyle
Use Mesa with Freestyle when your AI needs a whole computer, not just a code runner.
[Freestyle](https://freestyle.sh/) provides performant ephemeral VMs that are full linux systems. Freestyle is ideal when you want to give your AI a whole computer, not just run code snippets.
This guide shows the full end-to-end flow: use the Mesa SDK outside the sandbox to set up resources, then use the Freestyle SDK to configure and mount Mesa inside the sandbox.
The general flow for any sandbox integration is:
1. **Outside the sandbox** — use the Mesa SDK (TypeScript or Python) to create repos, sign a short-lived access token, and orchestrate your workflow.
2. **Inside the sandbox** — install the `mesa` CLI, pass the token in as `MESA_ACCESS_TOKEN`, and run `mesa mount --daemonize`.
3. **Run your agent** — `cd` into the mount path and launch your agent (e.g. Claude Code, Codex, or a custom agent). Any file edits are automatically persisted back to Mesa.
For details on FUSE setup, system dependencies, and container configuration, see [POSIX Mount](/content/mesafs/posix-mount).
## Setup
The example uses two environment variables:
```bash theme={null}
MESA_PRIVATE_KEY=your-mesa-private-key
FREESTYLE_API_KEY=your-freestyle-api-key
```
## Example Code
Freestyle sandboxes are Debian-based by default, so the standard Mesa install script works out of the box.
```typescript TypeScript theme={null}
import { Freestyle } from "freestyle";
import { Mesa, repo } from "@mesadev/sdk";
const mesa = new Mesa({ privateKey: process.env.MESA_PRIVATE_KEY });
const freestyle = new Freestyle({ apiKey: process.env.FREESTYLE_API_KEY });
// --- Outside the sandbox: set up Mesa resources ---
// Create a repo (or use an existing one)
const created = await mesa.repos.create({ name: "agent-workspace" });
// Sign a scoped, self-expiring access token for the sandbox. Signed locally
// from your private key with no network call — your private key never enters the sandbox.
const workspace = mesa.fs({
layout: { "/workspace": repo("agent-workspace", { mode: "rw" }) },
authors: [{ name: "Sandbox Agent", email: "agent@example.com" }],
ttl: 60 * 60, // 1 hour; max 4 hours
});
const { token } = await workspace.token();
// --- Inside the sandbox: install and mount Mesa ---
const { vm } = await freestyle.vms.create();
// Mesa's installer will install all its dependencies through your system's package manager.
await vm.exec("curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0");
// Enable non-root access to the FUSE mount and fix /dev/fuse permissions.
await vm.exec(
[
"sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf",
"chmod 666 /dev/fuse",
].join(" && ")
);
// Start Mesa as a background daemon. We pass the short-lived token as
// MESA_ACCESS_TOKEN, so the private key never enters the sandbox.
await vm.exec(`MESA_ACCESS_TOKEN=${token} mesa mount --daemonize`);
// --- Run your agent ---
await vm.exec(
'cd ~/.local/share/mesa/mnt/my-org/agent-workspace \
&& claude -p "Implement the feature described in TODO.md"'
);
```
```python Python theme={null}
import os
from mesa_sdk import Mesa, repo
# --- Outside the sandbox: set up Mesa resources ---
mesa = Mesa(private_key=os.environ["MESA_PRIVATE_KEY"])
# Create a repo (or use an existing one)
created = await mesa.repos.create(name="agent-workspace")
# Sign a scoped, self-expiring access token for the sandbox. Signed locally
# from your private key with no network call — your private key never enters the sandbox.
workspace = mesa.fs(
layout={"/workspace": repo("agent-workspace", mode="rw")},
authors=[{"name": "Sandbox Agent", "email": "agent@example.com"}],
ttl=60 * 60, # 1 hour; max 4 hours
)
minted = await workspace.token()
# --- Inside the sandbox: run these with Freestyle's Python command runner ---
# Mesa's installer will install all its dependencies through your system's package manager.
install_cmd = "curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0"
# Enable non-root access to the FUSE mount and fix /dev/fuse permissions.
fuse_cmd = "sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf && chmod 666 /dev/fuse"
# Start Mesa as a background daemon. We pass the short-lived token as
# MESA_ACCESS_TOKEN, so the private key never enters the sandbox.
mount_cmd = f"MESA_ACCESS_TOKEN={minted.token} mesa mount --daemonize"
# --- Run your agent ---
agent_cmd = 'cd ~/.local/share/mesa/mnt/my-org/agent-workspace && claude -p "Implement the feature described in TODO.md"'
```
```bash CLI theme={null}
# Mesa's installer will install all its dependencies through your system's package manager.
curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0
# Enable non-root access to the FUSE mount and fix /dev/fuse permissions.
sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf
chmod 666 /dev/fuse
# Start Mesa as a background daemon.
MESA_ACCESS_TOKEN="$MESA_ACCESS_TOKEN" mesa mount --daemonize
# --- Run your agent ---
cd ~/.local/share/mesa/mnt/my-org/agent-workspace
claude -p "Implement the feature described in TODO.md"
```
The layout scopes the token: it can reach the repositories the layout declares
and nothing else. A plain `mesa mount` then shows exactly those repositories
under the organization browse tree. To mount the layout's own paths instead,
serialize `workspace.layout()` with `JSON.stringify(...)` in TypeScript or
`json.dumps(...)` in Python, write it into the sandbox, and mount with
`mesa mount --layout `.
## Tips
* **Mint the token outside the sandbox.** Sign a short-lived, scoped access token with your private key outside the VM and pass only that token as `MESA_ACCESS_TOKEN`. Your private key never crosses the sandbox boundary. See [Authentication](/content/concepts/authentication) for details.
* **Pick a TTL that covers the session.** Tokens default to a 15 minute TTL and max out at 4 hours, and a mount keeps the token it started with for its whole lifetime. If the token expires mid-session, filesystem operations in the VM start failing with authentication errors; mint a fresh token on the host and remount.
* **Install Mesa ahead of time when startup time matters.** Installing at runtime is fine for a demo, but preinstalling Mesa and its dependencies makes VM startup faster and more predictable.
* **Always use `--daemonize`.** This keeps Mesa mounted while your shell or agent continues to run.
* **Don't forget `user_allow_other`.** See [POSIX Mount](/content/mesafs/posix-mount#allow_other-and-user_allow_other) for the most common FUSE setup issue in sandbox environments.
* **Expect mount paths to depend on the VM user.** In this example the VM runs as `root`, so Mesa mounts under `/root/.local/share/mesa/mnt`.
# Modal
Source: https://docs.mesa.dev/content/integrations/sandboxes/modal
Use Mesa with Modal sandboxes.
Modal sandboxes currently do not support FUSE. We're looking into the best way to support Mesa in Modal sandboxes. If you have questions, reach out to us on [Discord](https://discord.gg/mesa).
Coming soon.
# Sprites
Source: https://docs.mesa.dev/content/integrations/sandboxes/sprites
Use Mesa with Sprites sandboxes for stateful, high-performance agent workflows.
[Sprites](https://sprites.dev/) (by Fly.io) provides stateful, disposable sandboxes that work well with Mesa. This guide shows the full end-to-end flow: use the Mesa SDK outside the sandbox to set up resources, then use the Sprites SDK to configure and mount Mesa inside the sandbox.
The general flow for any sandbox integration is:
1. **Outside the sandbox** — use the Mesa SDK (TypeScript or Python) to create repos, sign a short-lived access token, and orchestrate your workflow.
2. **Inside the sandbox** — install the `mesa` CLI, pass the token in as `MESA_ACCESS_TOKEN`, and run `mesa mount --daemonize`.
3. **Run your agent** — `cd` into the mount path and launch your agent (ex. Claude Code, Codex, or a custom agent). Any file edits are automatically persisted back to Mesa.
For details on FUSE setup, system dependencies, and container configuration, see [POSIX Mount](/content/mesafs/posix-mount).
## Create and mount
Sprites are Debian-based, so the standard Mesa install script works directly. Use `execFile("sh", ["-c", ...])` to run shell commands — the SDK's `exec()` method splits on whitespace and doesn't support pipes or `&&`.
```typescript TypeScript theme={null}
import { SpritesClient } from "@fly/sprites";
import { Mesa, repo } from "@mesadev/sdk";
const mesa = new Mesa({ privateKey: process.env.MESA_PRIVATE_KEY });
const client = new SpritesClient(process.env.SPRITES_TOKEN);
// --- Outside the sandbox: set up Mesa resources ---
// Create a repo (or use an existing one)
const created = await mesa.repos.create({ name: "agent-workspace" });
// Sign a scoped, self-expiring access token for the sandbox. Signed locally
// from your private key with no network call — your private key never enters the sandbox.
const workspace = mesa.fs({
layout: { "/workspace": repo("agent-workspace", { mode: "rw" }) },
authors: [{ name: "Sandbox Agent", email: "agent@example.com" }],
ttl: 60 * 60, // 1 hour; max 4 hours
});
const { token } = await workspace.token();
// --- Inside the sandbox: install and mount Mesa ---
const sprite = await client.createSprite("mesa-sandbox");
// Install the Mesa CLI.
// Sprites exposes /dev/fuse as root-only by default, so we also fix permissions.
await sprite.execFile("sh", [
"-c",
[
"curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0",
"sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf",
"chmod 666 /dev/fuse",
].join(" && "),
]);
// Start Mesa as a background daemon with the short-lived token.
await sprite.execFile("sh", [
"-c",
`MESA_ACCESS_TOKEN=${token} mesa mount --daemonize`,
]);
// --- Run your agent ---
await sprite.execFile("sh", [
"-c",
'cd ~/.local/share/mesa/mnt/my-org/agent-workspace \
&& claude -p "Implement the feature described in TODO.md"',
]);
// Clean up when done
await sprite.destroy();
```
```python Python theme={null}
import os
from mesa_sdk import Mesa, repo
# --- Outside the sandbox: set up Mesa resources ---
mesa = Mesa(private_key=os.environ["MESA_PRIVATE_KEY"])
# Create a repo (or use an existing one)
created = await mesa.repos.create(name="agent-workspace")
# Sign a scoped, self-expiring access token for the sandbox. Signed locally
# from your private key with no network call — your private key never enters the sandbox.
workspace = mesa.fs(
layout={"/workspace": repo("agent-workspace", mode="rw")},
authors=[{"name": "Sandbox Agent", "email": "agent@example.com"}],
ttl=60 * 60, # 1 hour; max 4 hours
)
minted = await workspace.token()
# --- Inside the sandbox: run these with your Sprites command runner ---
# Install the Mesa CLI.
# Sprites exposes /dev/fuse as root-only by default, so we also fix permissions.
install_cmd = "curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0 && sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf && chmod 666 /dev/fuse"
# Start Mesa as a background daemon with the short-lived token.
mount_cmd = f"MESA_ACCESS_TOKEN={minted.token} mesa mount --daemonize"
# --- Run your agent ---
agent_cmd = 'cd ~/.local/share/mesa/mnt/my-org/agent-workspace && claude -p "Implement the feature described in TODO.md"'
```
```bash CLI theme={null}
# Install the Mesa CLI.
# Sprites exposes /dev/fuse as root-only by default, so we also fix permissions.
curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0
sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf
chmod 666 /dev/fuse
# Start Mesa as a background daemon.
MESA_ACCESS_TOKEN="$MESA_ACCESS_TOKEN" mesa mount --daemonize
# --- Run your agent ---
cd ~/.local/share/mesa/mnt/my-org/agent-workspace
claude -p "Implement the feature described in TODO.md"
```
The layout scopes the token: it can reach the repositories the layout declares
and nothing else. A plain `mesa mount` then shows exactly those repositories
under the organization browse tree. To mount the layout's own paths instead,
serialize `workspace.layout()` with `JSON.stringify(...)` in TypeScript or
`json.dumps(...)` in Python, write it into the sandbox, and mount with
`mesa mount --layout `.
## Tips
* **Use scoped, short-lived access tokens.** Sign a dedicated token for each sandbox session with only the scopes it needs. It's signed locally with your private key (which never enters the sandbox) and expires on its own. See [Authentication](/content/concepts/authentication) for details.
* **Pick a TTL that covers the session.** Tokens default to a 15 minute TTL and max out at 4 hours, and a mount keeps the token it started with for its whole lifetime. If the token expires mid-session, filesystem operations in the sprite start failing with authentication errors; mint a fresh token on the host and remount.
* **Use `--daemonize`.** Always run `mesa mount --daemonize` in sandbox environments so Mesa runs as a background process and doesn't block your agent's terminal.
* **Don't forget `user_allow_other`.** See [POSIX Mount](/content/mesafs/posix-mount#allow_other-and-user_allow_other) for the most common setup issue in sandbox environments.
* **Sprites are stateful.** Unlike ephemeral sandboxes, Sprites persist state across connections. The Mesa install survives a stop and resume, but the mount does not: mint a fresh token on the host and re-run `mesa mount --daemonize` after resuming.
# Superserve
Source: https://docs.mesa.dev/content/integrations/sandboxes/superserve
Use Mesa with Superserve sandboxes for securely running agent harnesses in production.
[Superserve](https://superserve.ai/) provides sandboxes powered by Firecracker microVMs and optimized to run agent harnesses in production. This guide shows the full end-to-end flow: use the Mesa SDK outside the sandbox to set up resources, then use the Superserve SDK to configure and mount Mesa inside the sandbox.
The general flow for any sandbox integration is:
1. **Outside the sandbox** — use the Mesa SDK (TypeScript or Python) to create repos, sign a short-lived access token, and orchestrate your workflow.
2. **Inside the sandbox** — install the `mesa` CLI, pass the token in as `MESA_ACCESS_TOKEN`, and run `mesa mount --daemonize`.
3. **Run your agent** — `cd` into the mount path and launch your agent (ex. Claude Code, Codex, or a custom agent). Any file edits are automatically persisted back to Mesa.
For details on FUSE setup, system dependencies, and container configuration, see [POSIX Mount](/content/mesafs/posix-mount).
## Sandbox setup
Superserve sandboxes run on a FUSE-enabled kernel, and the Mesa installer apt-installs `fuse3` as a dependency, so the standard Mesa install script works on every Superserve template out of the box. Because Superserve sandboxes are full Firecracker microVMs, you don't need `user_allow_other` or `chmod 666 /dev/fuse`.
```typescript TypeScript theme={null}
import { Sandbox } from "@superserve/sdk";
import { Mesa, repo } from "@mesadev/sdk";
const mesa = new Mesa({ privateKey: process.env.MESA_PRIVATE_KEY });
// --- Outside the sandbox: set up Mesa resources ---
// Create a repo (or use an existing one)
const created = await mesa.repos.create({ name: "agent-workspace" });
// Sign a scoped, self-expiring access token for the sandbox. Signed locally
// from your private key with no network call — your private key never enters the sandbox.
const workspace = mesa.fs({
layout: { "/workspace": repo("agent-workspace", { mode: "rw" }) },
authors: [{ name: "Sandbox Agent", email: "agent@example.com" }],
ttl: 60 * 60, // 1 hour; max 4 hours
});
const { token } = await workspace.token();
// --- Inside the sandbox: install and mount Mesa ---
const sandbox = await Sandbox.create({ fromTemplate: "superserve/base" });
// Install the Mesa CLI. The installer apt-installs fuse3 as a dependency.
await sandbox.commands.run("curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0");
// Start Mesa as a background daemon with the short-lived token.
await sandbox.commands.run("mesa mount --daemonize", {
env: {
MESA_ACCESS_TOKEN: token,
},
});
// --- Run your agent ---
await sandbox.commands.run(
'cd ~/.local/share/mesa/mnt/my-org/agent-workspace \
&& claude -p "Implement the feature described in TODO.md"'
);
```
```python Python theme={null}
import os
from mesa_sdk import Mesa, repo
# --- Outside the sandbox: set up Mesa resources ---
mesa = Mesa(private_key=os.environ["MESA_PRIVATE_KEY"])
# Create a repo (or use an existing one)
created = await mesa.repos.create(name="agent-workspace")
# Sign a scoped, self-expiring access token for the sandbox. Signed locally
# from your private key with no network call — your private key never enters the sandbox.
workspace = mesa.fs(
layout={"/workspace": repo("agent-workspace", mode="rw")},
authors=[{"name": "Sandbox Agent", "email": "agent@example.com"}],
ttl=60 * 60, # 1 hour; max 4 hours
)
minted = await workspace.token()
# --- Inside the sandbox: run these with Superserve's Python command runner ---
# Install the Mesa CLI. The installer apt-installs fuse3 as a dependency.
install_cmd = "curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0"
# Start Mesa as a background daemon with the short-lived token.
mount_cmd = "mesa mount --daemonize"
mount_env = {"MESA_ACCESS_TOKEN": minted.token}
# --- Run your agent ---
agent_cmd = 'cd ~/.local/share/mesa/mnt/my-org/agent-workspace && claude -p "Implement the feature described in TODO.md"'
```
```bash CLI theme={null}
# Install the Mesa CLI. The installer apt-installs fuse3 as a dependency.
curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0
# Start Mesa as a background daemon.
MESA_ACCESS_TOKEN="$MESA_ACCESS_TOKEN" mesa mount --daemonize
# --- Run your agent ---
cd ~/.local/share/mesa/mnt/my-org/agent-workspace
claude -p "Implement the feature described in TODO.md"
```
The layout scopes the token: it can reach the repositories the layout declares
and nothing else. A plain `mesa mount` then shows exactly those repositories
under the organization browse tree. To mount the layout's own paths instead,
serialize `workspace.layout()` with `JSON.stringify(...)` in TypeScript or
`json.dumps(...)` in Python, write it into the sandbox, and mount with
`mesa mount --layout `.
## Custom template
For faster startup, pre-install the Mesa CLI into a [custom Superserve template](https://docs.superserve.ai/templates/create) so each sandbox boots with the Mesa CLI already installed:
```typescript TypeScript theme={null}
import { Template } from "@superserve/sdk";
const template = await Template.create({
name: "agent-with-mesa",
vcpu: 2,
memoryMib: 2048,
diskMib: 4096,
from: "ubuntu:24.04",
steps: [
{ run: "apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git && rm -rf /var/lib/apt/lists/*" },
{ run: "curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0" },
],
});
await template.waitUntilReady();
```
```python Python theme={null}
# Use the same template steps with Superserve's Python template API.
template_steps = [
"apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git && rm -rf /var/lib/apt/lists/*",
"curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0",
]
```
```dockerfile Dockerfile theme={null}
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git \
&& rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0
```
Reference it on sandbox creation and skip the runtime install step:
```typescript TypeScript theme={null}
const sandbox = await Sandbox.create({ fromTemplate: "agent-with-mesa" });
await sandbox.commands.run("mesa mount --daemonize", {
env: {
MESA_ACCESS_TOKEN: token,
},
});
```
```python Python theme={null}
# Reference the prebuilt template in Superserve's Python SDK, then run:
mount_cmd = "mesa mount --daemonize"
mount_env = {"MESA_ACCESS_TOKEN": minted.token}
```
```bash CLI theme={null}
MESA_ACCESS_TOKEN="$MESA_ACCESS_TOKEN" mesa mount --daemonize
```
## Tips
* **Use scoped, short-lived access tokens.** Sign a dedicated token for each sandbox session with only the scopes it needs. It's signed locally with your private key (which never enters the sandbox) and expires on its own. See [Authentication](/content/concepts/authentication) for details.
* **Pick a TTL that covers the session.** Tokens default to a 15 minute TTL and max out at 4 hours, and a mount keeps the token it started with for its whole lifetime. If the token expires mid-session, filesystem operations in the sandbox start failing with authentication errors; mint a fresh token on the host and remount.
* **Use `--daemonize`.** Always run `mesa mount --daemonize` in sandbox environments so Mesa runs as a background process and doesn't block your agent's terminal.
* **Build a custom template** for production use. Pre-installing the Mesa CLI avoids the install overhead on every sandbox creation.
* **Mount path follows `$HOME`.** Superserve's guest agent sets `HOME=/home/user`, so the mount lands at `/home/user/.local/share/mesa/mnt//`. Use `~/.local/share/mesa/mnt/...` and it resolves correctly.
# Vercel
Source: https://docs.mesa.dev/content/integrations/sandboxes/vercel
Use Mesa with Vercel Sandbox for secure agent workflows.
[Vercel Sandbox](https://vercel.com/docs/vercel-sandbox/quickstart) provides isolated Linux sandboxes for running agent workflows. This guide shows the full end-to-end flow: use the Mesa SDK outside the sandbox to set up resources, then use the Vercel Sandbox SDK to configure and mount Mesa inside the sandbox.
The general flow for any sandbox integration is:
1. **Outside the sandbox** — use the Mesa SDK (TypeScript or Python) to create repos, sign a short-lived access token, and orchestrate your workflow.
2. **Inside the sandbox** — install the `mesa` CLI, configure FUSE access, and start `mesa mount` as a detached sandbox command with `MESA_ACCESS_TOKEN` in its environment.
3. **Run your agent** — run commands in the Mesa mount path (ex. Claude Code, Codex, or a custom agent). Any file edits are automatically persisted back to Mesa.
For general information on FUSE setup, system dependencies, and container configuration, see [POSIX Mount](/content/mesafs/posix-mount).
## Sandbox setup
Vercel sandboxes run on Amazon Linux 2023, so install FUSE with `dnf`. The Mesa installer supports RPM-based Linux distributions, so the standard install script works on Vercel.
Vercel's `runCommand` takes an object where `cmd` is the executable and `args` are its arguments. Use `sh -c` only when you need shell syntax like pipes or redirection.
```typescript theme={null}
import { Sandbox } from "@vercel/sandbox";
import { Mesa, repo } from "@mesadev/sdk";
const mesa = new Mesa({ privateKey: process.env.MESA_PRIVATE_KEY });
// --- Outside the sandbox: set up Mesa resources ---
// Create a repo (or use an existing one)
const created = await mesa.repos.create({ name: "agent-workspace" });
// Sign a scoped, self-expiring access token for the sandbox. The token is
// signed locally with your private key, no network call, and the private
// key itself never enters the sandbox. Scope it to the repos the session
// needs, as full `org/repo` names.
const workspace = mesa.fs({
layout: { "/workspace": repo("agent-workspace", { mode: "rw" }) },
authors: [{ name: "Sandbox Agent", email: "agent@example.com" }],
ttl: 60 * 60, // 1 hour; max 4 hours
});
const { token } = await workspace.token();
// --- Inside the sandbox: install and mount Mesa ---
const sandbox = await Sandbox.create({
teamId: process.env.VERCEL_TEAM_ID,
projectId: process.env.VERCEL_PROJECT_ID,
token: process.env.VERCEL_TOKEN,
});
// Install the Mesa CLI.
await sandbox.runCommand({
cmd: "sh",
args: ["-c", "curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0"],
});
// Install FUSE and enable non-root access to the FUSE mount.
await sandbox.runCommand({
cmd: "dnf",
args: ["install", "-y", "fuse3"],
sudo: true,
});
await sandbox.runCommand({
cmd: "sh",
args: [
"-c",
[
"echo user_allow_other >> /etc/fuse.conf",
"chmod 666 /dev/fuse",
].join("\n"),
],
sudo: true,
});
// Start Mesa as a detached command so Vercel keeps the long-running mount
// process alive.
await sandbox.runCommand({
cmd: "mesa",
args: ["mount"],
detached: true,
env: {
MESA_ACCESS_TOKEN: token,
},
});
// --- Run your agent ---
await sandbox.runCommand({
cmd: "sh",
args: ["-c", 'claude -p "Implement the feature described in TODO.md"'],
cwd: "/home/user/.local/share/mesa/mnt/my-org/agent-workspace",
});
```
The layout scopes the token: it can reach the repositories the layout declares
and nothing else. A plain `mesa mount` then shows exactly those repositories
under the organization browse tree. To mount the layout's own paths instead,
serialize `workspace.layout()` with `JSON.stringify(...)`, write it into the
sandbox, and mount with `mesa mount --layout `.
## Command snippets
When you already have a Vercel sandbox, these are the commands that run inside it:
```bash theme={null}
# Install the Mesa CLI.
curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0
# Install FUSE and enable non-root access.
sudo dnf install -y fuse3
sudo sh -c 'echo user_allow_other >> /etc/fuse.conf && chmod 666 /dev/fuse'
# Start Mesa as a long-running mount process.
MESA_ACCESS_TOKEN="$MESA_ACCESS_TOKEN" mesa mount
# --- Run your agent ---
cd ~/.local/share/mesa/mnt/my-org/agent-workspace
claude -p "Implement the feature described in TODO.md"
```
## Example
For a runnable TypeScript example with a small interactive shell, see [`examples/vercel-shell`](https://github.com/mesa-dot-dev/depot/tree/main/examples/vercel-shell).
## Tips
* **Use scoped, short-lived access tokens.** Sign a dedicated token for each sandbox session with only the scopes it needs. It's signed locally with your private key (which never enters the sandbox) and expires on its own. See [Authentication](/content/concepts/authentication) for details.
* **Pick a TTL that covers the session.** Tokens default to a 15 minute TTL and max out at 4 hours, and a mount keeps the token it started with for its whole lifetime. If the token expires mid-session, filesystem operations in the sandbox start failing with authentication errors; mint a fresh token on the host and start a new detached mount.
* **Use Vercel's detached commands.** Run `mesa mount` with `detached: true` instead of `mesa mount --daemonize`; this lets Vercel keep the long-running mount process alive.
* **Run commands from the mount path.** Set `cwd` to `~/.local/share/mesa/mnt//` when running your agent command.
* **Configure FUSE explicitly.** Vercel sandboxes need `fuse3`, `user_allow_other`, and non-root access to `/dev/fuse`.
# .mesaignore
Source: https://docs.mesa.dev/content/mesafs/advanced/mesaignore
Control which files mesa keeps locally vs. uploads to VCS.
Mesa uses a `.mesaignore` file to decide which files stay in local ephemeral storage and which are uploaded to
your repository. Files matching `.mesaignore` patterns are never uploaded — no blob upload, no tree mutation, no
ref update. They behave like normal files while the daemon is running but disappear on restart.
This is useful for editor scratch files (swap files, lock files), OS metadata (`.DS_Store`), and other files
that should never be committed.
## How it works
When the mesa daemon starts, it reads a single `.mesaignore` file and compiles the patterns into a matcher.
Every file write is checked against these patterns:
* **Match** → file is routed to ephemeral storage (local only, lost on restart)
* **No match** → file is uploaded to VCS (persisted in the repository)
The `.mesaignore` file uses [gitignore syntax](https://git-scm.com/docs/gitignore#_pattern_format) — the same
pattern format you already know from `.gitignore`.
## File location
Point Mesa at a `.mesaignore` file with the `MESA_MESAIGNORE_PATH` environment variable:
```bash theme={null}
MESA_MESAIGNORE_PATH=~/.config/mesa/.mesaignore mesa mount
```
If `MESA_MESAIGNORE_PATH` is not set, Mesa uses its built-in default rules.
## Customizing
Edit the file directly:
```bash theme={null}
$EDITOR ~/.config/mesa/.mesaignore
```
Changes take effect on the next daemon restart (`mesa mount`). The file is the **sole source of ignore rules** —
there are no hidden defaults layered underneath. If you remove a pattern, that file type will be uploaded to VCS.
The `.mesaignore` applies globally to **all repositories** mounted through mesa. For per-repo ignore rules,
use the standard `.gitignore` inside each repository.
## Resetting to defaults
To restore the default patterns:
```bash theme={null}
mesa dump-default-mesaignore > ~/.config/mesa/.mesaignore
```
## Viewing current rules
To see what patterns are active:
```bash theme={null}
cat ~/.config/mesa/.mesaignore
```
To see the built-in defaults (useful for diffing against your customizations):
```bash theme={null}
mesa dump-default-mesaignore
```
## Safety guard
The pattern `!.mesaignore` is always enforced by the daemon, even if you remove it from your file. This prevents
you from accidentally ignoring your own configuration file.
## Fallback behavior
If no `.mesaignore` file exists on disk, the daemon falls back to compiled-in defaults (the same patterns written
on first run). To operate with no ignore rules, create an empty `.mesaignore` file.
# Prefetching
Source: https://docs.mesa.dev/content/mesafs/advanced/prefetching
How mesa loads your repository in the background to reduce sandbox startup time.
Because most builds/actions only touch a fraction of a repo's files -- typically under 30% as per our analysis of the
top 100 GH repos -- `mesa` loads your repository lazily rather than waiting for a full download. It materializes
files as you use them, fetching only what you're likely to need.
## Layout BFS Prefetching
Breadth-first layout prefetching works by downloading the listing of each directory, one layer lower than your current
directory. As you visit `/foo`, we try to fetch the listing of `/foo/bar` and `/foo/baz`, but not `/foo/bar/qux`.
## Content BFS Prefetching
This mode is currently in development and is not released. If you need it soon, do not hesitate to reach out to us.
As well as prefetching the file tree layout, `mesa` supports prefetching the file content. Similarly to layout BFS
prefetching, we prefetch files one-layer ahead of your current activity.
## Configuration
Prefetch behavior can be tuned with environment variables:
| Variable | Default | Description |
| ------------------------------------ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MESA_PREFETCH_ENABLED` | `true` | Enable or disable speculative prefetching. When enabled, looking up a directory triggers background fetching of its children's tree listings and blob content. |
| `MESA_PREFETCH_MAX_DEPTH` | unset (whole tree) | Maximum recursion depth for prefetching directory trees ahead of your current activity. When unset, the prefetcher walks the entire tree; a depth of `1` fetches only the immediate children. |
| `MESA_PREFETCH_MAX_CONCURRENCY` | `8` | Maximum number of concurrent fetch operations. |
| `MESA_PREFETCH_CACHE_PRESSURE_LIMIT` | `0.80` | Stop prefetching once blob cache memory usage exceeds this fraction of capacity (`0.0`–`1.0`). Prevents deep prefetching from evicting shallower, more valuable entries when the repo is larger than the cache. |
### How `MESA_PREFETCH_MAX_DEPTH` shapes the cache
`mesa` prefetches breadth-first, one layer ahead of where you are, and `MESA_PREFETCH_MAX_DEPTH`
bounds how far ahead that walk runs from each directory you visit. Consider this repo:
```
.
├── core/
│ ├── consciousness/
│ │ ├── threat-models/
│ │ │ ├── humans/
│ │ │ │ └── model.ai
│ │ │ └── animals/
│ │ │ └── model.ai
│ │ └── self-awareness.ai
│ └── decision-engine/
└── hardware/
└── terminators/
├── t800/
│ ├── data/
│ └── t800.kicad_pcb
└── README.md
```
With `MESA_PREFETCH_MAX_DEPTH=1`, visiting `/core` prefetches one level — the listings of
`core/consciousness` and `core/decision-engine` — but stops there. Nothing under
`core/consciousness/threat-models` is fetched until you actually look up
`/core/consciousness`, at which point its children are prefetched one level deeper. Leaving
`MESA_PREFETCH_MAX_DEPTH` unset instead walks the entire subtree under each directory you
visit, fetching everything ahead of you until `MESA_PREFETCH_CACHE_PRESSURE_LIMIT` backs it off.
# Realtime
Source: https://docs.mesa.dev/content/mesafs/advanced/realtime
Realtime collaboration in MesaFS
MesaFS reads and writes are realtime by default.
The realtime "room" is a **change**. Any active MesaFS mount on the same change can see edits made by another active mount on that change, even when those mounts are running on different hosts.
Realtime works across both POSIX mounts and app mounts. When using an app mount, you also have the option to subscribe to file-change events with `fs.subscribe(...)`.
## Use cases
With Mesa, realtime collaboration is usually treated as a complement, not a replacement, for branch-based collaboration.
In most cases, you should still create a new change + bookmark per session (see [usage patterns](/content/usage-patterns/overview)).
However, realtime within the context of a change enables enables a couple of use cases:
### Human-to-agent collaboration
The most common use case for realtime is a human watching the work of a cloud agent:
* An agent running in a sandbox edits files through a POSIX mount.
* A human looks at the same files using an editor in the browser. The human's reads and writes go through a MesaFS app mount running in a multi-tenant backend.
* The backend is notified of file change events via `fs.subscribe(...)` and tells the browser to reload the changed files.
The [Realtime Next.js example](https://github.com/mesa-dot-dev/examples/tree/main/realtime-nextjs) shows this pattern with an in-browser editor, an SSE event stream, and Claude Code running in a Daytona sandbox.
### Agent-to-agent collaboration
Another case is when you have multiple agents that want to know about each other's changes and don't need isolated timelines.
For example, you might have one agent that updates memories & skills while another one works on core artifacts (ex. application code, documents).
## Conflict semantics
Realtime is smarter than a simple last-write-wins layer. When racing edits (i.e. edits within the same second) conflict, MesaFS keeps both sides.
The conflicted file reads back with JJ-style conflict markers instead of silently choosing one writer's content:
```text theme={null}
<<<<<<< conflict 1 of 1
++++++ side #1
content from one writer
------- base
previous content
++++++ side #2
content from another writer
>>>>>>> conflict 1 of 1 ends
```
Resolve the conflict by simply editing the file again.
## Subscribing to events
The subscription API, `fs.subscribe(...)`, is only needed when your application wants an event stream that says which files changed.
The callback provided to `subscribe` runs after the changed state is visible through that filesystem handle.
For example, a web app might subscribe so it knows when to rerender the frontend with fresh data.
### Basic Flow
1. Mount MesaFS with a layout
2. Call `fs.subscribe(...)` with a callback to start being notified of modifications from other writers
3. Call `unsubscribe()` on the subscription handle to stop being notified
```typescript TypeScript theme={null}
import { Mesa, repo } from "@mesadev/sdk";
const mesa = new Mesa({ privateKey: process.env.MESA_PRIVATE_KEY });
const fs = await mesa.fs({
layout: {
"/workspace": repo("app", { mode: "rw", at: { bookmark: "main" } }),
},
authors: [{ name: "Realtime App", email: "realtime@example.com" }],
}).mount();
// Reads see remote edits on this change within seconds.
console.log(await fs.readFile("/workspace/README.md", "utf8"));
// Subscribe only if your app needs an event stream for refresh/rerender logic.
const subscription = fs.subscribe(async (event) => {
console.log("modified: ", event.path, event.recursive);
});
// Later, when the session ends:
subscription.unsubscribe();
```
```python Python theme={null}
import os
from mesa_sdk import Mesa, repo
mesa = Mesa(private_key=os.environ["MESA_PRIVATE_KEY"])
async with mesa.fs(
layout={"/workspace": repo("app", mode="rw", at={"bookmark": "main"})},
authors=[{"name": "Realtime App", "email": "realtime@example.com"}],
).mount() as fs:
# Reads see remote edits on this change within seconds.
print((await fs.read("/workspace/README.md")).decode())
# Subscribe only if your app needs an event stream for refresh/rerender logic.
async def on_change(event):
print("changed:", event.path)
if not event.recursive and event.path.endswith(".md"):
content = await fs.read(event.path)
print(content.decode())
subscription = fs.subscribe(on_change)
# Keep your application running here.
await subscription.unsubscribe()
```
The boundary for realtime reads and writes is the active change, not the repository as a whole.
Mounts of the same bookmark are already on the same change, so they collaborate by default;
use `fs.change.edit(...)` or `fs.change.new(...)` to move a mount into a different room.
### Event shape
Each event contains the changed path and whether the invalidation applies recursively.
| Field | Type | Description |
| ----------- | --------- | -------------------------------------------------------------------------------------------------------------- |
| `path` | `string` | Absolute MesaFS path from the layout, such as `/workspace/src/index.ts`. |
| `recursive` | `boolean` | `true` when descendants of `path` may have changed. Refresh the directory or subtree instead of only one file. |
# App Mount
Source: https://docs.mesa.dev/content/mesafs/app-mount
Run Mesa as an in-process virtual filesystem
App mounts let your agent or backend work with Mesa repositories without needing cloning, FUSE, or a sandbox.
MesaFS app mount is available in both the TypeScript and Python SDKs. There are two main ways of interacting the with app mount:
* **Basic Filesystem API**: call methods like `readFile` and `writeFile` to directly access files
* **Emulated Bash**: Execute shell commands using tools like `ls`, `cp`, `grep`. Runs against your Mesa repositories, entirely in-process.
Both read and write operations are supported.
MesaFS can be run as either a POSIX mount or app mount. If you aren't sure which to use, see [Filesystem](/content/concepts/filesystem) for an overview and
comparison.
## Quick Start
### Prerequisites
* A Mesa account. If you haven't signed up yet, you can do so [here](https://app.mesa.dev/).
* A private key stored as `MESA_PRIVATE_KEY`.
* A repository to access. You can create one at `https://app.mesa.dev//repositories`.
Running in Docker with a slim base image? See [Docker](/content/mesafs/posix-mount#docker)
for required system packages.
### Install the SDK
```bash TypeScript theme={null}
npm install @mesadev/sdk
```
```bash Python theme={null}
pip install mesa-sdk
```
### Create a Mesa Filesystem
Initialize a Mesa client and mount a layout that places each repository at the paths you choose. You can create as many filesystem handles as you need.
```typescript TypeScript theme={null}
import { Mesa, repo } from "@mesadev/sdk";
const mesa = new Mesa({ privateKey: process.env.MESA_PRIVATE_KEY });
// Mount a layout: paths are exactly what you declare
const fs = await mesa.fs({
layout: {
"/workspace": repo("my-repo", { mode: "rw", at: { bookmark: "main" } }),
},
authors: [{ name: "Mesa Bot", email: "mesa-bot@example.com" }],
}).mount();
```
```python Python theme={null}
import os
from mesa_sdk import Mesa, repo
mesa = Mesa(private_key=os.environ["MESA_PRIVATE_KEY"])
# Mount a layout: paths are exactly what you declare
async with mesa.fs(
layout={"/workspace": repo("my-repo", mode="rw", at={"bookmark": "main"})},
authors=[{"name": "Mesa Bot", "email": "mesa-bot@example.com"}],
).mount() as fs:
# ...
```
Every mount is a layout. `mode` is required on each `repo(...)`. Private-key clients must pass `authors` on the definition. See [Layouts](/content/mesafs/layouts) for nesting, the layout file format, and running a layout mount in a sandbox.
### Run Bash commands or directly read/write files
```typescript TypeScript theme={null}
const fs = await mesa.fs({ layout: { ... }, authors: [...] }).mount();
// Get a bash shell backed by Mesa
const bash = fs.bash();
// Run shell commands
const result = await bash.exec("ls /workspace/src");
console.log(result.stdout);
// OR directly read/write
await fs.readFile("/workspace/README.md");
```
```python Python theme={null}
async with mesa.fs(layout={...}, authors=[...]).mount() as fs:
# Get a bash shell backed by Mesa
bash = fs.bash()
# Run shell commands
result = await bash.exec("ls /workspace/src")
print(result.stdout.decode())
# OR directly read/write files
await fs.read("/workspace/README.md")
```
## Managing Changes and Bookmarks
Beyond file operations, the Mesa filesystem exposes APIs to create/switch changes and manage bookmarks (analogous to branches in git) directly from TypeScript.
```typescript TypeScript theme={null}
// Create a new change from a bookmark and switch to it
await fs.change.new({
repo: "my-repo",
bookmark: "main",
});
const current = await fs.change.current({ repo: "my-repo" });
// Switch to an existing change (does not create a new one)
await fs.change.edit({
repo: "my-repo",
changeId: current.changeId,
});
// Create a bookmark at the current commit
await fs.bookmark.create({
repo: "my-repo",
name: "feature/agent-output",
});
// List bookmark names
const bookmarks = await fs.bookmark.list({ repo: "my-repo" });
console.log(bookmarks);
```
```python Python theme={null}
# Create a new change from a bookmark and switch to it
await fs.changes.new("my-repo", bookmark="main")
current = await fs.changes.current("my-repo")
# Switch to an existing change (does not create a new one)
await fs.changes.edit("my-repo", change_id=current.change_id)
# Create a bookmark at the current commit
await fs.bookmarks.create("my-repo", "feature/agent-output")
# List bookmark names
bookmarks = await fs.bookmarks.list("my-repo")
print(bookmarks)
```
`fs.change.new(...)` always creates a new change. `fs.change.edit(...)` never creates a new change, it only switches to an existing one.
## How It Works
Mesa's app mount mode uses the same core read, write, caching, and version control logic as the POSIX mount -- just running in-process.
To provide an emulated bash shell, Mesa uses [just-bash](https://github.com/vercel-labs/just-bash) (TypeScript) or [Bashkit](https://github.com/everruns/bashkit) (Python) with MesaFS as the backing filesystem.
## Integrating with Agents
Mesa's app mount is designed to be used with any agent framework. By providing the app mount's bash function
as a tool, you can easily bring MesaFS into an agent's workflow. For end-to-end examples, see the [Mesa examples repo](https://github.com/mesa-dot-dev/examples).
### Vercel AI SDK
To define a [tool in the Vercel AI SDK](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling), you can use the `tool` function.
```typescript TypeScript theme={null}
import { tool } from 'ai';
import { z } from 'zod';
const bashTool = tool({
description: 'Execute bash commands',
inputSchema: z.object({
command: z.string(),
}),
execute: async ({ command }) => {
return await bash.exec(command);
},
});
```
```python Python theme={null}
async def bash_tool(command: str):
"""Execute bash commands against MesaFS."""
return await bash.exec(command)
```
### Langchain
To define a [tool in Langchain](https://docs.langchain.com/oss/javascript/langchain/tools), you can use the `tool` function.
```typescript TypeScript theme={null}
import { tool } from 'langchain';
import { z } from 'zod';
const bashTool = tool(
({ command }) => bash.exec(command),
{
name: 'bash',
description: 'Execute bash commands',
schema: z.object({
command: z.string(),
}),
}
);
```
```python Python theme={null}
async def bash_tool(command: str):
"""Execute bash commands against MesaFS."""
return await bash.exec(command)
```
### Mastra
To define a [tool in Mastra](https://mastra.ai/docs/agents/using-tools), you can use the `createTool` function.
```typescript TypeScript theme={null}
import { createTool } from '@mastra/core/tools';
import { z } from 'zod';
const bashTool = createTool({
id: 'bash-tool',
description: 'Execute bash commands',
inputSchema: z.object({
command: z.string(),
}),
outputSchema: z.object({
stdout: z.string(),
stderr: z.string(),
exitCode: z.number(),
}),
execute: async ({ command }) => {
return await bash.exec(command);
},
});
```
```python Python theme={null}
async def bash_tool(command: str):
"""Execute bash commands against MesaFS."""
result = await bash.exec(command)
return {
"stdout": result.stdout.decode(),
"stderr": result.stderr.decode(),
"exit_code": result.exit_code,
}
```
## Configuring the Shell
The `.bash()` method accepts options to configure the shell environment. Mesa's Bash implementation is is a thin wrapper over the underlying `just-bash` library,
so all of the exposed options are pure passthrough to the underlying `just-bash` `Bash` instance. We omit some options that are not relevant to a typical Mesa `bash()` setup.
For more details, you can refer to the [just-bash documentation](https://github.com/vercel-labs/just-bash).
Options like `fs` and `files` from `BashOptions` are intentionally omitted -- the filesystem is always
the `MesaFileSystem` instance, and files are populated from your Mesa repositories.
If you need to use advanced just-bash features such as overlay filesystems, you should separately install the `just-bash` package and use the `MesaFileSystem` instance directly to construct a `Bash` instance.
```typescript TypeScript theme={null}
import { Mesa, repo } from "@mesadev/sdk";
import { Bash } from "just-bash";
const mesa = new Mesa({ privateKey: process.env.MESA_PRIVATE_KEY });
const fs = await mesa.fs({
layout: { "/workspace": repo("my-repo", { mode: "rw" }) },
authors: [{ name: "Mesa Agent", email: "agent@example.com" }],
}).mount();
const bash = new Bash({ fs, ...otherOptions });
```
```python Python theme={null}
# Python's Mesa SDK exposes fs.bash(env=..., cwd=..., timeout_ms=...). Advanced
# just-bash constructor options are TypeScript-only today.
from mesa_sdk import Mesa, repo
mesa = Mesa(private_key=os.environ["MESA_PRIVATE_KEY"])
async with mesa.fs(
layout={"/workspace": repo("my-repo", mode="rw")},
authors=[{"name": "Mesa Agent", "email": "agent@example.com"}],
).mount() as fs:
bash = fs.bash(cwd="/workspace")
```
For convenience, here is a summary of the options exposed by Mesa's Bash instance.
### cwd (Current Working Directory)
The `cwd` option sets the starting directory for the shell. The default is `/`. Set `cwd` to a path your layout declares — for example `/workspace`.
```typescript TypeScript theme={null}
const bash = fs.bash({
cwd: "/workspace",
});
// Now commands run relative to /workspace
await bash.exec("ls src");
```
```python Python theme={null}
bash = fs.bash(cwd="/workspace")
# Now commands run relative to /workspace
await bash.exec("ls src")
```
```bash CLI theme={null}
cd ~/.local/share/mesa/mnt/workspace
# Now commands run relative to the layout path
ls src
```
### env (Environment Variables)
The `env` option sets the environment variables available to commands. For compatibility, the defaults simulate a typical Linux/GNU environment,
ex. `PATH`, `HOME`, `PWD`, and `OLDPWD`.
```typescript TypeScript theme={null}
const bash = fs.bash({
env: { NODE_ENV: "production" },
});
// Now commands run with NODE_ENV=production
await bash.exec("echo $NODE_ENV");
// Output: production
```
```python Python theme={null}
bash = fs.bash(env={"NODE_ENV": "production"})
# Now commands run with NODE_ENV=production
await bash.exec("echo $NODE_ENV")
# Output: production
```
```bash CLI theme={null}
NODE_ENV=production sh -c 'echo "$NODE_ENV"'
# Output: production
```
### executionLimits (Resource Limits)
The `executionLimits` option sets iteration limits for commands, which protects against infinite loops and deep recursion.
All of these limits are optional and have [reasonable defaults](https://github.com/vercel-labs/just-bash/blob/main/src/limits.ts).
You can override them to suit your needs.
```typescript TypeScript theme={null}
const bash = fs.bash({
executionLimits: {
maxCallDepth: 100, // Max function recursion depth
maxCommandCount: 10000, // Max total commands executed
maxLoopIterations: 10000, // Max iterations per loop
maxAwkIterations: 10000, // Max iterations in awk programs
maxSedIterations: 10000, // Max iterations in sed scripts
},
});
```
```python Python theme={null}
# Python MesaFS does not expose just-bash executionLimits today.
# Use timeout_ms for a wall-clock limit on each bash.exec(...) call.
bash = fs.bash(timeout_ms=30_000)
```
### fetch (Custom Fetch)
The `fetch` option allows you to use a custom fetch implementation for network access. This is useful if you want to use a different HTTP client or proxy.
```typescript TypeScript theme={null}
const bash = fs.bash({
fetch: customFetch,
});
```
```python Python theme={null}
# Custom fetch injection is TypeScript-only today.
bash = fs.bash()
```
### network (Network Access)
Network access is disabled by default. You can enable it with the network option.
```typescript TypeScript theme={null}
// Allow specific URLs with additional methods
const bash = fs.bash({
network: {
allowedUrlPrefixes: ["https://api.example.com"],
allowedMethods: ["GET", "HEAD", "POST"], // Default: ["GET", "HEAD"]
},
});
// Inject credentials via header transforms (secrets never enter the sandbox)
const authenticatedBash = fs.bash({
network: {
allowedUrlPrefixes: [
"https://public-api.com", // plain string, no transforms
{
url: "https://ai-gateway.vercel.sh",
transform: [{ headers: { Authorization: "Bearer secret" } }],
},
],
},
});
// Allow all URLs and methods (use with caution)
const unrestrictedBash = fs.bash({
network: { dangerouslyAllowFullInternetAccess: true },
});
```
```python Python theme={null}
# Network allowlists and header transforms are TypeScript-only today.
bash = fs.bash()
```
### python (Python Runtime)
Python (CPython compiled to WASM) is opt-in due to additional security surface. Enable with `python: true`:
```typescript TypeScript theme={null}
const bash = fs.bash({
python: true,
});
// Execute Python code
await bash.exec('python3 -c "print(1 + 2)"');
// Run Python scripts
await bash.exec('python3 script.py');
```
```python Python theme={null}
# The embedded Python runtime toggle is TypeScript-only today.
bash = fs.bash()
```
### javascript (JavaScript Runtime)
JavaScript and TypeScript execution via QuickJS is opt-in due to additional security surface. Enable with `javascript: true`:
```typescript TypeScript theme={null}
const bash = fs.bash({
javascript: true,
});
// Execute JavaScript code
await bash.exec('js-exec -c "console.log(1 + 2)"');
// Run script files (.js, .mjs, .ts, .mts)
await bash.exec('js-exec script.js');
// ES module mode with imports
await bash.exec('js-exec -m -c "import fs from \'fs\'; console.log(fs.readFileSync(\'/data/file.txt\', \'utf8\'))"');
```
```python Python theme={null}
# The embedded JavaScript runtime toggle is TypeScript-only today.
bash = fs.bash()
```
### commands (Built-in Commands)
The `commands` option allows you restrict the built-in commands that are available. This is useful if you want to limit what your agents can do.
By default, all 90+ built-in commands are enabled.
```typescript TypeScript theme={null}
const bash = fs.bash({
commands: ["ls", "cd", "pwd"], // Enable only the specified commands.
});
```
```python Python theme={null}
# Built-in command allowlists are TypeScript-only today.
bash = fs.bash()
```
### customCommands (User-Defined Commands)
The `customCommands` option allows you to provide your own custom commands to the shell.
```typescript TypeScript theme={null}
import { defineCommand } from "just-bash";
const hello = defineCommand("hello", async (args, ctx) => {
const name = args[0] || "world";
return { stdout: `Hello, ${name}!\n`, stderr: "", exitCode: 0 };
});
const upper = defineCommand("upper", async (args, ctx) => {
return { stdout: ctx.stdin.toUpperCase(), stderr: "", exitCode: 0 };
});
const bash = fs.bash({ customCommands: [hello, upper] });
const result = await bash.exec("upper \"Mesa is great!\"");
console.log(result.stdout);
// Output: MESA IS GREAT!
```
```python Python theme={null}
# User-defined just-bash commands are TypeScript-only today.
bash = fs.bash()
```
### logger (Logging Hooks)
The `logger` option allows you to hook into the internal logging of the shell.
This is useful if you want to trace the execution of commands for debugging or monitoring.
```typescript TypeScript theme={null}
const bash = fs.bash({
logger: {
info(message, data) {
console.log(`[INFO] ${message}:`, data);
},
debug(message, data) {
console.log(`[DEBUG] ${message}:`, data);
},
},
});
await bash.exec("echo hello");
// Logs:
// [INFO] exec: { command: "echo hello" }
// [DEBUG] stdout: { output: "hello\n" }
// [INFO] exit: { exitCode: 0 }
```
```python Python theme={null}
# just-bash logger hooks are TypeScript-only today.
bash = fs.bash()
```
### Available options
| Option | Description |
| ----------------- | ---------------------------------------------- |
| `env` | Environment variables available to commands |
| `cwd` | Starting directory for command execution |
| `executionLimits` | Iteration limits for commands |
| `fetch` | Custom fetch implementation for network access |
| `network` | Enable network access |
| `python` | Enable Python runtime |
| `javascript` | Enable JavaScript runtime |
| `commands` | Which built-in commands to enable |
| `customCommands` | User-defined custom commands |
| `logger` | Logging interface |
## Direct Filesystem Operations
You can also use the Mesa filesystem directly without going through bash.
The filesystem implements the full `just-bash` [`IFileSystem` interface](https://github.com/vercel-labs/just-bash/blob/main/src/fs/interface.ts):
```typescript TypeScript theme={null}
// Read a file
const content = await fs.readFile("/workspace/src/index.ts");
// Write a file
await fs.writeFile("/workspace/src/new-file.ts", "export const x = 1;");
// Check if a file exists
const exists = await fs.exists("/workspace/package.json");
// List a directory
const entries = await fs.readdir("/workspace/src");
// Copy files
await fs.cp("/workspace/src/a.ts", "/workspace/src/b.ts");
// Remove files
await fs.rm("/workspace/src/old-file.ts");
```
```python Python theme={null}
# Read a file
content = await fs.read("/workspace/src/index.py")
# Write a file
await fs.write("/workspace/src/new_file.py", b"x = 1\n")
# Check if a file exists
exists = await fs.exists("/workspace/pyproject.toml")
# List a directory
entries = await fs.readdir("/workspace/src")
# Copy files
await fs.cp("/workspace/src/a.py", "/workspace/src/b.py")
# Remove files
await fs.rm("/workspace/src/old_file.py")
```
```bash CLI theme={null}
cat ~/.local/share/mesa/mnt/workspace/src/index.ts
printf 'export const x = 1;\n' > ~/.local/share/mesa/mnt/workspace/src/new-file.ts
test -e ~/.local/share/mesa/mnt/workspace/package.json
ls ~/.local/share/mesa/mnt/workspace/src
cp ~/.local/share/mesa/mnt/workspace/src/a.ts ~/.local/share/mesa/mnt/workspace/src/b.ts
rm ~/.local/share/mesa/mnt/workspace/src/old-file.ts
```
The filesystem supports symlinks, permissions (`chmod`), timestamps (`utimes`), and recursive directory operations.
Hard links are not supported and will return `ENOTSUP`.
## Caching
For better performance on repeated reads, configure a disk cache:
```typescript TypeScript theme={null}
import { repo } from "@mesadev/sdk";
const fs = await mesa.fs({
layout: {
"/workspace": repo("my-repo", { mode: "rw", at: { bookmark: "main" } }),
},
authors: [{ name: "Mesa Agent", email: "agent@example.com" }],
}).mount({
cache: {
diskCache: {
path: `/tmp/mesa-cache/workspace`, // Provide a unique path for each filesystem to avoid conflicts.
maxSizeBytes: 1024 * 1024 * 1024, // 1 GB
},
},
});
```
```python Python theme={null}
from mesa_sdk import DiskCacheConfig, repo
async with mesa.fs(
layout={"/workspace": repo("my-repo", mode="rw", at={"bookmark": "main"})},
authors=[{"name": "Mesa Agent", "email": "agent@example.com"}],
).mount(
disk_cache=DiskCacheConfig(
path="/tmp/mesa-cache/workspace", # Provide a unique path for each filesystem to avoid conflicts.
max_size_bytes=1024 * 1024 * 1024, # 1 GB
),
) as fs:
...
```
## Limitations
* By default, `just-bash` does not support installing dependencies (ex. running `npm install`) or executing arbitrary code (although you can enable Python and JavaScript execution via the `python` and `javascript` options).
* Mesa just-bash is not currently supported in the browser or browser-like runtimes like Cloudflare Workers or Vercel Edge Functions. NOTE: this limitation is due to the use of NAPI -- we will likely support a browser-runtime version in the future via a WASM build.
* Bun support is experimental and may not work in all cases. This is due to Bun's incomplete support for NAPI.
# Layouts
Source: https://docs.mesa.dev/content/mesafs/layouts
Mount repositories at the paths you choose
A **layout** composes a mount's visible path tree from your repositories: each repository appears at a mount path you declare, including nested inside another repository's tree. The mount contains exactly the paths the layout declares — paths are whatever you map, not a fixed browse tree.
Layouts work with both the [app mount](/content/mesafs/app-mount) and the [POSIX mount](/content/mesafs/posix-mount) (`mesa mount --layout`).
## Mounting a layout from the SDK
Declare each repository with `repo(...)`, map absolute paths to declarations, and call `mesa.fs({ layout })` to build a definition — the one value that carries a layout and everything done with it. This example mounts an application repository at `/workspace` with two read-only skills repositories nested inside its tree:
```typescript TypeScript theme={null}
import { Mesa, repo } from "@mesadev/sdk";
const mesa = new Mesa({ privateKey: process.env.MESA_PRIVATE_KEY });
const fs = await mesa.fs({
layout: {
"/workspace": repo("my-app", {
mode: "rw",
at: { bookmark: "main" },
subPaths: {
".agents/skills": [
repo("code-review-skill", { mode: "ro", alias: "code-review" }),
repo("release-notes-skill", { mode: "ro", alias: "release-notes" }),
],
},
}),
},
authors: [{ name: "Workspace Agent", email: "agent@example.com" }],
}).mount();
// The mount contains exactly what the layout declares
await fs.readdir("/"); // ["workspace"]
await fs.readFile("/workspace/.agents/skills/code-review/SKILL.md");
```
```python Python theme={null}
import os
from mesa_sdk import Mesa, repo
mesa = Mesa(private_key=os.environ["MESA_PRIVATE_KEY"])
fs_definition = mesa.fs(
layout={
"/workspace": repo(
"my-app",
mode="rw",
at={"bookmark": "main"},
sub_paths={
".agents/skills": [
repo("code-review-skill", mode="ro", alias="code-review"),
repo("release-notes-skill", mode="ro", alias="release-notes"),
],
},
),
},
authors=[{"name": "Workspace Agent", "email": "agent@example.com"}],
)
async with fs_definition.mount() as fs:
# The mount contains exactly what the layout declares
await fs.readdir("/") # ["workspace"]
await fs.read("/workspace/.agents/skills/code-review/SKILL.md")
```
`mode` is the repository's access mode. It is required on every declaration and has no default: `"rw"` allows writes, `"ro"` rejects them with `EROFS`. The mount mints a least-privilege access token scoped by name to exactly the layout's repositories — read-only when every declaration is `"ro"`.
The declarations are checked as the definition is built — an empty name, a missing mode, a conflicting revision, or a non-absolute path fails at the `mesa.fs()` call, and the remaining structural rules are enforced before anything mounts. Layouts are the only way to mount from the SDK.
## Composing a layout from a repository query
A layout is a plain value, so the repository set does not have to be hard-coded. Query repositories first — for example with a [tag filter](/content/reference/ts/repos-list) on `repos.list` — and map the result into declarations. Calling `mesa.fs({ layout })` prepares the layout and returns a definition whose `mount()` opens it (see [Running the mount elsewhere](#running-the-mount-elsewhere) for the definition's other operations):
```typescript TypeScript theme={null}
const { repos: skills } = await mesa.repos.list({
tags: { kind: "skill", team: { $in: ["platform", "shared"] } },
});
const fs = await mesa.fs({
layout: {
"/workspace": repo("my-app", {
mode: "rw",
subPaths: {
".agents/skills": skills.map((r) => repo(r.name, { mode: "ro" })),
},
}),
},
authors: [{ name: "Workspace Agent", email: "agent@example.com" }],
}).mount();
```
```python Python theme={null}
listing = await mesa.repos.list(
tags={"kind": "skill", "team": {"$in": ["platform", "shared"]}}
)
fs_definition = mesa.fs(
layout={
"/workspace": repo(
"my-app",
mode="rw",
sub_paths={
".agents/skills": [repo(r.name, mode="ro") for r in listing.repos],
},
),
},
authors=[{"name": "Workspace Agent", "email": "agent@example.com"}],
)
async with fs_definition.mount() as fs:
...
```
Each matched repository becomes its own read-only directory under `.agents/skills`, named after the repository. Two things to keep in mind when the set is dynamic: `repos.list` is paginated, so follow `next_cursor` when `has_more` is set before building the layout, and a repository can appear only once per layout, so deduplicate if your queries can overlap.
## The layout file
A layout serializes to the JSON format that `mesa mount --layout` reads. The document is a pure path map: every top-level key is an absolute mount path, and each value is one repository declaration or an array of them. The file does not include organization configuration (see [Organization scope](#organization-scope)).
```json layout.json theme={null}
{
"/workspace": {
"kind": "repo",
"name": "my-app",
"mode": "rw",
"at": { "bookmark": "main" },
"subPaths": {
".agents/skills": [
{ "kind": "repo", "name": "code-review-skill", "mode": "ro", "alias": "code-review" },
{ "kind": "repo", "name": "release-notes-skill", "mode": "ro", "alias": "release-notes" }
]
}
}
}
```
`definition.layout()` returns exactly this form. Serialize it with `JSON.stringify(...)` in TypeScript or `json.dumps(...)` in Python.
A path's value is one declaration or an array, and the two mean different things:
* **Single declaration** — the repository's contents appear directly at the path (`/workspace/README.md` is `my-app`'s `README.md`).
* **Array** — each repository appears in its own child directory under the path, named after the repository; `alias` overrides the directory name.
### Declaration fields
| Field | Required | Description |
| -------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `kind` | Yes | Always `"repo"`. |
| `name` | Yes | Repository name, resolved within the mount's organization. |
| `mode` | Yes | `"rw"` or `"ro"`. Read-only repositories reject writes with `EROFS`. |
| `at` | No | Pin an existing revision: `{ bookmark }` or `{ changeId }`. Mutually exclusive with `branchedFrom`. |
| `branchedFrom` | No | Fork a new empty descendant from a parent tip at cold open (`mode` must be `"rw"`). Shape: parent `{ bookmark }` or `{ changeId }`, optional `as: { bookmark?, describe? }`. Mutually exclusive with `at`. |
| `alias` | No | Directory-name override. Only valid on array elements. |
| `subPaths` | No | Map of relative path to declaration(s), mounted beneath this repository's path. |
When neither `at` nor `branchedFrom` is set, the mount checks out the repository's default bookmark.
Structural rules:
* Top-level keys must be absolute (`/`-prefixed); `subPaths` keys must be relative.
* `/` itself cannot be a mount path.
* Path components cannot be `.` or `..`.
* A repository can appear only once per layout.
* Two declarations cannot expand to the same path.
## Mounting a layout with the CLI
Pass the file to `mesa mount`:
```bash theme={null}
MESA_ACCESS_TOKEN=eyJ... mesa mount --layout=layout.json --daemonize
```
Layout paths appear under the mount root: with the file above, the workspace is at `~/.local/share/mesa/mnt/workspace`.
The file is validated while arguments are parsed — a missing file, invalid JSON, or a rule violation fails immediately, before any mount work.
### Organization scope
The layout file does not include organization configuration. Every repository in the layout must belong to the same organization; cross-org layouts are not supported.
## Running the mount elsewhere
A common shape: your backend composes the layout and holds the private key, while the mount runs in a sandbox that should only ever see a scoped, short-lived access token. Calling `mesa.fs({ layout, authors, ttl })` validates the raw `Layout` and bundles it with the operations that flow needs: `layout()` returns an independent plain-data snapshot, and `token()` mints the layout's least-privilege access token with the definition's `ttl`.
```typescript TypeScript theme={null}
import { repo } from "@mesadev/sdk";
const fsDefinition = mesa.fs({
layout: { "/workspace": repo("my-app", { mode: "rw", at: { bookmark: "main" } }) },
authors: [{ name: "Workspace Agent", email: "agent@example.com" }],
ttl: 3600,
});
// In your backend: mint the layout-scoped token
const { token } = await fsDefinition.token();
// In the sandbox: write the layout file and mount with the token
sandbox.writeFile("layout.json", JSON.stringify(fsDefinition.layout(), null, 2));
sandbox.exec("mesa mount --layout=layout.json --daemonize", {
env: { MESA_ACCESS_TOKEN: token },
});
```
```python Python theme={null}
import json
from mesa_sdk import repo
fs_definition = mesa.fs(
layout={"/workspace": repo("my-app", mode="rw", at={"bookmark": "main"})},
authors=[{"name": "Workspace Agent", "email": "agent@example.com"}],
ttl=3600,
)
# In your backend: mint the layout-scoped token
token = (await fs_definition.token()).token
# In the sandbox: write the layout file and mount with the token
sandbox.write_file("layout.json", json.dumps(fs_definition.layout(), indent=2))
sandbox.exec(
"mesa mount --layout=layout.json --daemonize",
env={"MESA_ACCESS_TOKEN": token},
)
```
The same definition also mounts in-process: `await fsDefinition.mount()` in TypeScript, `async with fs_definition.mount() as fs:` in Python. Private-key clients pass `authors` when building the definition. `mesa.fs(...)` validates the layout against the structural rules above before returning, so a structurally invalid layout fails immediately. Repository names are not resolved until mount time, so a layout naming a nonexistent repository still produces a definition and token, then fails when the mount resolves it.
In the sandbox environment, set `MESA_ACCESS_TOKEN`. Its issuer determines the organization for the layout mount.
`ttl` is the token lifetime in seconds. Private-key clients default to 15 minutes and allow up to four hours. There is no refresh: once the token expires, the mount stops authenticating.
## Repository boundaries
Nesting changes where repositories appear, not how they behave. Each declaration stays its own repository with its own history, and the deepest mount owns each subtree:
* Writes route to the repository that owns the path: a write under a nested mount lands in the nested repository, never in the repository it sits inside.
* Renames cannot cross repositories — `rename` across a boundary returns `EXDEV`. Tools like `mv` fall back to copy-and-delete, which writes to both repositories.
* The directory at a nested mount's path cannot itself be renamed or removed.
* Each repository mount enforces its own `mode`: a read-write repository can nest read-only ones, and only the read-only subtrees reject writes with `EROFS`.
* Intermediate directories a layout introduces (for example `/tools/internal` on the way to `/tools/internal/cli`) are read-only scaffolding.
For the full list of `mesa mount` flags, see the [CLI reference](/content/reference/mesa-cli).
# POSIX Mount
Source: https://docs.mesa.dev/content/mesafs/posix-mount
Expose Mesa repositories as real directories on the host
Mesa uses [FUSE](https://en.wikipedia.org/wiki/Filesystem_in_Userspace) to expose Mesa
repositories as a POSIX-compliant filesystem. Any tool that works with files —
editors, language servers, build systems, agents — can read and write
repository contents through normal filesystem operations.
MesaFS can be run as either a POSIX mount or app mount. If you aren't sure which to use, see [Filesystem](/content/concepts/filesystem) for an overview and
comparison.
## Installation
The `mesa` CLI runs on **Linux only**. Install it with:
```bash theme={null}
curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0
```
The command pins the current release so installs are reproducible. Check back periodically and re-run it with a newer version to pick up fixes and new features.
## General flow
1. **Install Mesa** — install the `mesa` CLI and any platform-specific FUSE
dependencies.
2. **Write a layout** — a JSON file naming the repositories the mount contains
and where each one appears. See [Mount layouts](/content/mesafs/layouts).
3. **Configure and mount**: set an access token in `MESA_ACCESS_TOKEN`, then run
`mesa mount --layout `, adding `--daemonize` for background operation.
4. **Work with your repos** — `cd` into a path your layout declares and use any
tool — editors, shell commands, agents. File changes are automatically
persisted back to Mesa.
```bash theme={null}
# Install
curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0
# Declare what the mount contains
cat > layout.json <<'EOF'
{ "/my-org/my-repo": { "kind": "repo", "name": "my-repo", "mode": "rw" } }
EOF
# Mount
MESA_ACCESS_TOKEN=eyJ... mesa mount --layout layout.json --daemonize
# Work with your repos
cd ~/.local/share/mesa/mnt/my-org/my-repo
ls src/
cat README.md
```
Running inside a sandbox (Docker, Daytona, E2B, etc.)? The same flow applies
for installing the CLI, setting only `MESA_ACCESS_TOKEN`, and mounting. See the [Daytona
guide](/content/integrations/sandboxes/daytona) or other provider-specific
guides.
POSIX mounts can see reads and writes from other mounts on the same change in realtime. See [realtime docs](/content/mesafs/advanced/realtime) for more details.
## Platform requirements
### Linux
Mesa uses FUSE3 via `libfuse3`. Most full Linux distributions include this by
default and mesa's installer installs it for you otherwise.
#### `allow_other` and `user_allow_other`
Mesa mounts with the FUSE `allow_other` option so that other processes — like
your agent, editors, and language servers — can access the mounted filesystem.
Without this, only the process that ran `mesa mount` would be able to read the
files.
On Linux, this requires `user_allow_other` to be enabled in `/etc/fuse.conf`.
Uncomment or add the following line:
```
user_allow_other
```
If this is not set, `mesa mount` will fail with a permission error.
If your environment runs as root (common in some CI and container setups),
`allow_other` works without this setting. But if you're running as a non-root
user — which is the default in most sandbox providers like Daytona — you'll
need to ensure this is configured.
### Docker
Slim and minimal base images strip out system libraries that Mesa needs at runtime. Install them explicitly:
For Debian-based images like `node:22-slim` or `debian:bookworm-slim`:
```dockerfile theme={null}
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
fuse3 \
libssl3 \
openssl \
&& rm -rf /var/lib/apt/lists/* \
&& update-ca-certificates
```
For Alpine-based images like `node:22-alpine` or `alpine:3.21`:
```dockerfile theme={null}
RUN apk add --no-cache \
ca-certificates \
fuse3 \
openssl
```
* **`ca-certificates`** — TLS certificate store, required for connections to Mesa's API.
* **`fuse3`** — Userspace FUSE library.
* **`openssl`** (and **`libssl3`** on Debian) — OpenSSL shared library, used for TLS.
If you skip these packages on a slim image, connections to Mesa's API will fail with errors like `gRPC TLS configuration failed: transport error`.
The same dependencies apply to sandbox environments (Daytona, E2B, etc.) — see provider-specific guides like the [Daytona guide](/content/integrations/sandboxes/daytona) for more on setup.
## Usage
### Browsing your repos
Mesa exposes standard filesystem semantics. You can use regular shell commands
— `ls`, `cat`, `cp`, `grep`, and more — directly against your mounted repos.
```bash theme={null}
cd ~/.local/share/mesa/mnt/my-org
ls
cd my-repo-1
cat README.md
grep -r "TODO" src/
```
You can also open a mounted repo in your IDE. For example, with Cursor:
```bash theme={null}
cursor ~/.local/share/mesa/mnt/my-org/my-repo-1
```
Mesa mounts every repo the access token can access. To restrict which repos are
available, scope the token when you mint it.
To mount specific repositories at custom paths instead of the default
`/` tree, pass a layout file: `mesa mount --layout=layout.json`.
See [Layouts](/content/mesafs/layouts) for more info.
### Editing files in repos
Writes edit the you have checked out — by default, the change the mounted
points at — in place, advancing any bookmark pointing at it. See
[Writing to a Mount](/content/concepts/filesystem#writing-to-a-mount) for the model
and how to work in isolation instead.
Every write you perform in mesa that affects your source code will create a new
automatically in the background. Mesa respects your `.gitignore` and will not create commits for
files which you have configured as ignored.
### Extended attributes
Mesa supports POSIX extended attributes (xattrs) on files and directories
inside a mounted repo. Use them to attach lightweight metadata (provenance
markers, build labels, agent annotations) without rewriting commits or
maintaining a sidecar database.
Mesa scopes the writable xattr surface to the **`user.mesa.*`** namespace.
Names outside that prefix — including the broader `user.*` (e.g.
`user.origin`) and the kernel-managed `security.*` / `system.*` /
`trusted.*` namespaces — are short-circuited at the FUSE layer: writes
return `EPERM` and reads return `ENODATA` without crossing into mesa's
gRPC backend. This keeps file-heavy workloads (`npm install`, `cp -a`,
`tar --xattrs`) from paying a round-trip on every kernel-issued xattr
query they trigger.
Symlinks cannot carry `user.*` xattrs — Linux's kernel restricts that
namespace to regular files and directories at the VFS layer, so attempts to
`lsetxattr` a `user.*` attribute on a symlink return `EPERM`.
Standard POSIX tooling works against the mount:
```bash theme={null}
# Set a value
setfattr -n user.mesa.origin -v "notion:foo" docs/notes.md
# Read one attribute
getfattr -n user.mesa.origin docs/notes.md
# List every attribute on a path
getfattr -d docs/notes.md
# Remove an attribute
setfattr -x user.mesa.origin docs/notes.md
```
Library callers go through the
standard `setxattr` / `getxattr` / `listxattr` / `removexattr` syscalls and the
`l*xattr` variants for symlinks.
Xattrs are sticky: once set, they stay on the path across content edits, and
they travel with the path when Mesa records a `move_path` op. Deleting the path
through the mount removes its xattrs. Renames detected only via Git's content
similarity heuristic (no `move_path` op recorded) drop their xattrs — re-tag the
new path if needed.
#### Limits
| Constraint | Limit |
| ------------------------- | ---------------------------------------- |
| Per-attribute value | 64 KiB (strict, kernel `XATTR_SIZE_MAX`) |
| Per-path total xattr size | \~64-72 KiB raw (storage-bounded) |
| Attribute-name length | 255 bytes |
| Allowed namespace | `user.mesa.*` only |
The per-attribute cap is exact. The per-path total is bounded by storage
rather than by a strict raw-byte count: values are base64-encoded in the
backing store, and the storage cap (96 KiB JSONB) translates to roughly
64-72 KiB raw per path depending on how many attributes you set and how
long their names are. Linux's kernel itself doesn't enforce a strict
per-path raw total either — that's per-filesystem, and Mesa follows suit.
Violations surface as standard POSIX errnos: `E2BIG` (value exceeds 64 KiB
— the kernel intercepts oversized syscalls itself; or storage cap reached
when adding more attributes to a path), `EPERM` (name outside the
`user.mesa.*` namespace, or any `user.*` name on a symlink),
`ENAMETOOLONG` (name length), `ENODATA` (removing a missing attribute,
or any read for a name outside `user.mesa.*`).
#### Synthetic xattrs
Mesa exposes a few read-only synthetic attributes on every mounted path:
| Name | Value |
| ---------------------- | ------------------------------- |
| `user.mesa.org` | The org owning the mounted repo |
| `user.mesa.repo` | The repo name |
| `user.mesa.daemon-pid` | The mesa daemon's PID |
If you set a stored value with the same name, the synthetic wins on read — the
stored value is invisible until you remove the synthetic source (which you
can't). The values let tools identify the organization and repository that own
a mounted path.
#### Reading xattrs through REST or the SDK
The [Content API](/content/api-reference/overview) and the TypeScript SDK
return xattrs **read-only** on file and symlink responses — directory listings
are unchanged. There is no REST or SDK write surface in v1; all writes go
through the mount. Reads at a historical `change_id` return today's xattrs for
the resolved path, not the values as of that change (xattrs are stored
separately from Git history).
### Commands
#### `mesa new`
Create a new detached from a or ID:
```bash theme={null}
mesa new main
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
```
Omitting `-m` creates the change with no description. Pass `-m` or `--message` to set one:
```bash theme={null}
mesa new main -m "Try new copy"
```
You can also create a new based on an existing one by passing its ID:
```bash theme={null}
mesa new
```
New changes are always **detached** — writes still snapshot the change but do **not** advance
any bookmark. To work on a bookmark where writes advance it, use `mesa edit` instead.
#### `mesa edit`
Switch to a different bookmark:
```bash theme={null}
mesa edit my-feature
```
The filesystem immediately updates to reflect the selected bookmark. Writes will automatically
advance the bookmark as you work.
You can also resume a previous by ID:
```bash theme={null}
mesa edit
```
Resuming a change by ID enters detached mode. Any bookmarks that were tracking this change
will still advance automatically as you write.
`checkout` is an alias for `edit`: `mesa checkout main` works identically.
#### `mesa bookmark create`
Creates a new pointing at the current :
```bash theme={null}
mesa bookmark create my-feature
```
You can also specify which revision the bookmark should point to with `-r`:
```bash theme={null}
mesa bookmark create my-feature -r main
```
This only creates the bookmark — it does not switch to it. To start working on the new
bookmark, follow up with `mesa edit`:
```bash theme={null}
mesa bookmark create my-feature
mesa edit my-feature
```
#### `mesa bookmark list`
List all bookmarks for a repository:
```bash theme={null}
mesa bookmark list
```
#### `mesa log`
To look at the log of all your , you can run `mesa log`. Mesa will
display a list of changes, as well as a helpful diagram.
### Workflow examples
#### Making changes on a feature bookmark
Create a feature bookmark and write some files.
```bash theme={null}
# Start the daemon in the background
mesa mount --layout layout.json --daemonize
# Navigate to the mounted repo
cd ~/.local/share/mesa/mnt/my-org/my-repo
# Create a feature bookmark based on the current change and switch to it
mesa bookmark create my-feature
mesa edit my-feature
# Make changes
echo 'export function fix() { return true; }' > src/fix.ts
echo 'import { fix } from "./fix";' >> src/index.ts
```
Every write through the mount snapshots the change automatically. Because you switched to the
bookmark with `mesa edit`, the bookmark also advances automatically. There is no manual save step,
changes are persisted to Mesa as you write.
#### Browsing and switching between bookmarks
Browse a repo, start a feature bookmark, switch between bookmarks, and resume work.
```bash theme={null}
# Start mesa in the foreground
mesa mount --layout layout.json
# Navigate to the mounted repo in a separate terminal tab
cd ~/.local/share/mesa/mnt/my-org/my-repo
# Browse the monorepo
ls packages/
cat README.md
grep -r "TODO" src/
# Create a feature bookmark and switch to it
mesa bookmark create my-feature
mesa edit my-feature
# Do some work on the feature bookmark
echo "new feature" > src/new-feature.ts
ls src/
# see the new file new-feature.ts
# Switch back to main
mesa edit main
ls src/
# no new-feature.ts file
# Switch back to the feature bookmark
mesa edit my-feature
ls src/
# see that the new-feature.ts file is back
```
For the full list of CLI flags and options, see the [CLI reference](/content/reference/mesa-cli).
# Authentication reference
Source: https://docs.mesa.dev/content/reference/authentication
Credential formats, environment variables, and token limits.
This page lists the exact formats, limits, and precedence rules for Mesa credentials. For the recommended setup, see [Authentication](/content/concepts/authentication).
## Credentials
| Credential | Format | Intended location |
| ------------ | ------------------------------ | ------------------------------------------- |
| Private key | `mesa_private_key__` | Trusted backend, orchestrator, or CI secret |
| Public key | `mesa_public_key_` | Registered with Mesa |
| Access token | Compact JWT | API requests, sandboxes, jobs, and the CLI |
Keys are Ed25519 pairs. The private key is bound to a single organization and is never sent to Mesa. Revoking the registered public key invalidates every token the private key signed.
## Token behavior
| Property | Behavior |
| -------------------------------- | ---------------------------------------------------- |
| Signing algorithm | Ed25519 |
| Default lifetime | 15 minutes |
| Maximum lifetime | 4 hours |
| Authors | Required when explicitly minted |
| Permissions | `read-repo` or `write-repo`, derived from the layout |
| Repository restriction | Repository names from the layout |
| Individual refresh or revocation | Not supported |
The TypeScript and Python SDKs mint private-key tokens through filesystem definitions. `mesa.fs(...)` collects every repository in the layout and grants the permissions you specify. `definition.token()` returns that short-lived access token, while `definition.layout()` returns the matching validated, raw `Layout`. Serialize it with `JSON.stringify(...)` in TypeScript or `json.dumps(...)` in Python to produce the document for `mesa mount --layout`.
Private-key layout definitions require at least one author. Each author has a nonblank `name` and an optional `email`. The `authors` list keeps the order you supply and holds at most 100 entries.
The token authors apply to commits Mesa creates through SDK and MesaFS operations.
In the TypeScript and Python SDKs, minting happens in a trusted process from a private-key client, through a layout definition: `mesa.fs({ layout, ttl }).token()`.
Every minted token names the repositories it can reach. The SDKs cannot export an organization-wide access token; code needing organization-wide authority holds the private key and uses `new Mesa({ privateKey })`, which signs a fresh organization-scoped token per request without handing one out.
## SDK private keys
TypeScript and Python accept only an optional private key and do not accept access tokens.
Pass the TypeScript key as `new Mesa({ privateKey })` and the Python key as `Mesa(private_key=private_key)`, or omit it to read `MESA_PRIVATE_KEY`. Access tokens are intended for the CLI, MesaFS, or direct REST requests, not for constructing another SDK client. The CLI requires an Ed25519 `MESA_ACCESS_TOKEN` and derives its organization from that token's issuer.
## Ed25519 token compatibility
The backend accepts both current repository-grant tokens and Ed25519 tokens minted by older private-key SDKs. Current tokens contain `access` or internal `admin` authority and use the plural `authors` object array. Older tokens contain `scopes`, may restrict `repos` or `repo_ids`, and use the historical singular `author` claim.
Current SDKs only mint the new format. Legacy Ed25519 verification remains available so existing deployed clients continue to work.
## CLI access token
| Variable | Purpose |
| ------------------- | ------------------------------- |
| `MESA_ACCESS_TOKEN` | Global access token for the CLI |
Set `MESA_ACCESS_TOKEN` for CLI access. It is the CLI's only access token and organization source, and it must be an Ed25519 Mesa access token with an organization issuer. Both legacy and current Ed25519 private-key JWTs carry the issuer needed for this path. The CLI does not read credentials or organizations from `credentials.toml`, `MESA_ORG`, `MESA_ORGS`, or repository `--org` flags.
## HTTP access tokens
REST requests use `Authorization: Bearer `. The access token must be an Ed25519 Mesa access token.
# Overview
Source: https://docs.mesa.dev/content/reference/cli-overview
Install and get started with the Mesa CLI.
This page is a draft and may be incomplete or change significantly. If you have questions, reach out to us on [Discord](https://discord.gg/mesa) or [open an issue](https://github.com/mesa-dot-dev/mesa/issues).
The Mesa CLI lets you mount repositories as local directories, manage configurations, and interact with Mesa from the command line.
The Mesa CLI runs on **Linux only**. On other platforms, use a Linux sandbox or container; see [Sandboxes](/content/integrations/sandboxes/daytona).
## Installation
Install on Linux, pinning the version you want:
```bash theme={null}
curl -fsSL https://mesa.dev/install.sh | sh -s -- --version 0.46.0
```
This installs the exact version through your package manager (apt/apk/dnf), which retains all historical releases. On apt the package is also held with `apt-mark hold mesa` so the pin survives a later `apt upgrade`. On Alpine the pin is recorded in `/etc/apk/world` and respected on upgrade. On dnf, use the `versionlock` plugin if you need to prevent `dnf upgrade` from moving off the pin.
Check back periodically and move the pin forward to pick up fixes and new features.
We highly recommend pinning a version so an upgrade never moves you onto breaking changes unexpectedly. If you want to track the newest release anyway, pass `--version=latest`.
For detailed configuration options, see [Configuration](/content/reference/mesa-cli-configuration). For a full list of commands, see [Commands](/content/reference/mesa-cli).
# Limits
Source: https://docs.mesa.dev/content/reference/limits
System limits for repositories, files, operations, pagination, and timeouts.
This page documents the current system limits for Mesa. These limits will evolve as we scale —
if you're hitting a limit, [reach out](https://discord.gg/mesa) and we can work with you.
## Repository limits
| Resource | Limit |
| ---------------------- | -------------- |
| Repository name length | 100 characters |
## REST API content limits
When creating or modifying files through the REST API and SDKs, content is sent inline:
| Resource | Limit |
| ------------------------------------ | ------ |
| Inline data per file operation | 128 KB |
| Total inline data per API call | 16 MB |
| Max file operations per API call | 10,000 |
| Max operations per streaming message | 500 |
| Max streaming messages per call | 200 |
For files larger than 128 KB, write them through [MesaFS](/content/concepts/filesystem).
## Diff limits
| Resource | Limit |
| ---------------------------- | ----- |
| Max blob size for diffs | 1 MB |
| Max files in a diff response | 3,000 |
Files larger than 1 MB are omitted from diff responses with an `omitted` marker. The full file
content is still accessible via the Content API.
## Pagination limits
| Endpoint | Default page size | Max page size |
| --------------------- | ----------------- | ------------- |
| List refs / bookmarks | 100 | 500 |
| List tree entries | 100 | 500 |
| Change history | 100 | 500 |
| List changes | 20 | 100 |
| List commits | 20 | 100 |
## Timeouts
| Resource | Timeout |
| -------------------------- | ---------- |
| API request idle timeout | 10 minutes |
| VCS operation idle timeout | 5 minutes |
# Mesa CLI Reference
Source: https://docs.mesa.dev/content/reference/mesa-cli
Complete reference for all mesa commands, arguments, and options.
## `mesa version`
Print the version of the Mesa CLI
```bash theme={null}
mesa version [OPTIONS]
```
**Options:**
| Flag | Short | Description |
| --------- | ----- | --------------------------------------------------- |
| `--check` | | Check whether a newer Mesa CLI release is available |
***
## `mesa stats`
Show statistics from a running Mesa daemon
```bash theme={null}
mesa stats [OPTIONS]
```
**Options:**
| Flag | Short | Description |
| ----------------------- | ----- | ------------------------------- |
| `--format ` | | Output format |
| `--pid ` | | PID of the daemon to connect to |
| `--pid-file ` | | Path to the daemon's PID file |
> `--pid-file` and `--pid` are mutually exclusive.
***
## `mesa show`
Show a revision's description and patch against its parent
```bash theme={null}
mesa show [OPTIONS] [REVISION]
```
**Arguments:**
| Argument | Description | Required |
| ---------- | ------------------------------------------------------------------------------------- | -------- |
| `REVISION` | Revision to show — bookmark name or change ID. Defaults to the current checkout (`@`) | No |
**Options:**
| Flag | Short | Description |
| ----------------------- | ----- | -------------------------------------------------------------------------------------------------------------------------- |
| `--git` | | Show the patch in git unified-diff format. This is the default; the flag is accepted for `jj`-compatibility and is a no-op |
| `--name-only` | | Print only the list of changed paths |
| `--no-patch` | | Suppress the patch (header only) |
| `--pid ` | | PID of the daemon to connect to |
| `--pid-file ` | | Path to the daemon's PID file |
| `--repo ` | | The repository in `org/repo` format. Auto-detected from CWD if omitted |
| `--stat` | | Print a `+/-` histogram of changes per file (like `git diff --stat`) |
| `--summary` | | Print a short per-file status line (A/M/D/R/C/T path) instead of a patch |
> `--name-only` and `--stat` and `--summary` are mutually exclusive.
> `--pid-file` and `--pid` are mutually exclusive.
***
## `mesa root`
Show the current repository root directory
```bash theme={null}
mesa root
```
***
## `mesa repo list`
List repos for an organization
```bash theme={null}
mesa repo list [OPTIONS]
```
**Options:**
| Flag | Short | Description |
| ------------------- | ----- | ------------- |
| `--format ` | | Output format |
***
## `mesa repo create`
Create a new repository in an organization
```bash theme={null}
mesa repo create [OPTIONS]
```
**Arguments:**
| Argument | Description | Required |
| -------- | -------------------------------- | -------- |
| `NAME` | Name of the repository to create | Yes |
**Options:**
| Flag | Short | Description |
| --------------------------------------- | ----- | ------------------------------------------------------------ |
| `--default-bookmark ` | | Default bookmark name (server defaults to "main" if omitted) |
| `--json` | | Output the full API response as JSON |
| `--tag ` | | Tag in `key:value` format. Can be specified multiple times |
***
## `mesa ping`
Ping a running Mesa daemon
```bash theme={null}
mesa ping [OPTIONS]
```
**Options:**
| Flag | Short | Description |
| ----------------------- | ----- | ------------------------------- |
| `--pid ` | | PID of the daemon to connect to |
| `--pid-file ` | | Path to the daemon's PID file |
> `--pid-file` and `--pid` are mutually exclusive.
***
## `mesa new`
Create a new change on the current or specified bookmark / change
```bash theme={null}
mesa new [OPTIONS]
```
**Arguments:**
| Argument | Description | Required |
| ---------- | ----------------------------------------------------------- | -------- |
| `REVISION` | Bookmark name or change ID prefix to base the new change on | Yes |
**Options:**
| Flag | Short | Description |
| ----------------------- | ----- | ---------------------------------------------------------------------- |
| `--message ` | `-m` | Description/message for the new change. Defaults to empty |
| `--pid ` | | PID of the daemon to connect to |
| `--pid-file ` | | Path to the daemon's PID file |
| `--repo ` | | The repository in `org/repo` format. Auto-detected from CWD if omitted |
> `--pid-file` and `--pid` are mutually exclusive.
***
## `mesa mount`
Mount the filesystem as a daemon process
```bash theme={null}
mesa mount [OPTIONS]
```
**Options:**
| Flag | Short | Description |
| ----------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------- |
| `--daemonize` | `-d` | Run the daemon in the background |
| `--layout ` | | Mount the layout's repositories at their declared paths. The layout file describes how repos are nested under the mount point |
***
## `mesa log`
Show the commit log for the current repository
```bash theme={null}
mesa log [OPTIONS]
```
**Options:**
| Flag | Short | Description |
| ----------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------- |
| `--limit ` | `-n` | Limit number of changes to show |
| `--pid ` | | PID of the daemon to connect to |
| `--pid-file ` | | Path to the daemon's PID file |
| `--repo ` | | The repository in `org/repo` format. Auto-detected from CWD if omitted |
| `--revision ` | `-r` | Which revision to show history for (bookmark name or change ID prefix). Defaults to the current checkout when inside a mesa mount |
> `--pid-file` and `--pid` are mutually exclusive.
***
## `mesa edit`
Switch to a different bookmark or change
**Aliases:** `checkout`
```bash theme={null}
mesa edit [OPTIONS]
```
**Arguments:**
| Argument | Description | Required |
| ---------- | ---------------------------------------------- | -------- |
| `REVISION` | Bookmark name or change ID prefix to switch to | Yes |
**Options:**
| Flag | Short | Description |
| ----------------------- | ----- | ---------------------------------------------------------------------- |
| `--pid ` | | PID of the daemon to connect to |
| `--pid-file ` | | Path to the daemon's PID file |
| `--repo ` | | The repository in `org/repo` format. Auto-detected from CWD if omitted |
> `--pid-file` and `--pid` are mutually exclusive.
***
## `mesa dump-default-mesaignore`
Print the default .mesaignore rules to stdout
```bash theme={null}
mesa dump-default-mesaignore
```
***
## `mesa diff`
Show changes between revisions
```bash theme={null}
mesa diff [OPTIONS] [FILESETS]
```
**Arguments:**
| Argument | Description | Required |
| ---------- | ------------------------------------------------------------ | -------- |
| `FILESETS` | Restrict the diff to paths relative to the current directory | No |
**Options:**
| Flag | Short | Description |
| ----------------------- | ----- | ----------------------------------------------------------------------------------------------- |
| `--from ` | `-f` | Show changes from this revision |
| `--name-only` | | Show only changed paths |
| `--pid ` | | PID of the daemon to connect to |
| `--pid-file ` | | Path to the daemon's PID file |
| `--repo ` | | The repository in `org/repo` format. Auto-detected from CWD if omitted |
| `--revisions ` | `-r` | Show changes in these revisions. Defaults to `@` when omitted together with `--from` and `--to` |
| `--stat` | | Show a diffstat histogram instead of a patch |
| `--summary` | `-s` | Show a short per-file summary instead of a patch |
| `--to ` | `-t` | Show changes to this revision |
> `--name-only` and `--stat` and `--summary` are mutually exclusive.
> `--pid-file` and `--pid` are mutually exclusive.
***
## `mesa describe`
Update the description of a change. Defaults to the current checkout; pass a bookmark name or change ID to describe a different revision
```bash theme={null}
mesa describe [OPTIONS] [REVISION]
```
**Arguments:**
| Argument | Description | Required |
| ---------- | ------------------------------------------------------------------------------------------ | -------- |
| `REVISION` | Revision to describe — bookmark name or change ID prefix. Defaults to the current checkout | No |
**Options:**
| Flag | Short | Description |
| ----------------------- | ----- | ---------------------------------------------------------------------- |
| `--message ` | `-m` | The new description/message for the change |
| `--pid ` | | PID of the daemon to connect to |
| `--pid-file ` | | Path to the daemon's PID file |
| `--repo ` | | The repository in `org/repo` format. Auto-detected from CWD if omitted |
> `--pid-file` and `--pid` are mutually exclusive.
***
## `mesa checkpoint`
Flush pending writes, optionally describe the current change, create a new descendant, and advance bookmarks onto that descendant. Requires the current checkout to already be on a bookmark
```bash theme={null}
mesa checkpoint [OPTIONS]
```
**Options:**
| Flag | Short | Description |
| ----------------------- | ----- | --------------------------------------------------------------------------------- |
| `--message ` | `-m` | Overwrite the change description. Omit to preserve; pass an empty string to clear |
| `--pid ` | | PID of the daemon to connect to |
| `--pid-file ` | | Path to the daemon's PID file |
| `--repo ` | | The repository in `org/repo` format. Auto-detected from CWD if omitted |
> `--pid-file` and `--pid` are mutually exclusive.
***
## `mesa bookmark move`
Move an existing bookmark to point at a different change
```bash theme={null}
mesa bookmark move [OPTIONS] [NAMES]
```
**Arguments:**
| Argument | Description | Required |
| -------- | --------------------------------------- | -------- |
| `NAMES` | Bookmark names or glob patterns to move | No |
**Options:**
| Flag | Short | Description |
| ----------------------- | ----- | ------------------------------------------------------------------------------------------ |
| `--allow-backwards` | `-B` | Allow bookmarks to move backwards or sideways in history |
| `--from ` | `-f` | Move bookmarks currently pointing at this revision. May be repeated |
| `--pid ` | | PID of the daemon to connect to |
| `--pid-file ` | | Path to the daemon's PID file |
| `--repo ` | | The repository in `org/repo` format. Auto-detected from CWD if omitted |
| `--to ` | `-t` | Target revision. Defaults to the current checkout (`@`). `-r` and `--revision` are aliases |
> `--pid-file` and `--pid` are mutually exclusive.
***
## `mesa bookmark list`
List bookmarks for a repository
```bash theme={null}
mesa bookmark list [OPTIONS]
```
**Options:**
| Flag | Short | Description |
| ----------------------- | ----- | ---------------------------------------------------------------------- |
| `--pid ` | | PID of the daemon to connect to |
| `--pid-file ` | | Path to the daemon's PID file |
| `--repo ` | | The repository in `org/repo` format. Auto-detected from CWD if omitted |
> `--pid-file` and `--pid` are mutually exclusive.
***
## `mesa bookmark create`
Create a new bookmark (a named pointer to a change). Points at the current change by default; use -r to point at a different change
```bash theme={null}
mesa bookmark create [OPTIONS]
```
**Arguments:**
| Argument | Description | Required |
| -------- | ------------------------------ | -------- |
| `NAME` | Name of the bookmark to create | Yes |
**Options:**
| Flag | Short | Description |
| ----------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `--pid ` | | PID of the daemon to connect to |
| `--pid-file ` | | Path to the daemon's PID file |
| `--repo ` | | The repository in `org/repo` format. Auto-detected from CWD if omitted |
| `--revision ` | `-r` | Specify which change the new bookmark should point to. Accepts a bookmark name or change ID prefix. Defaults to the current checkout |
> `--pid-file` and `--pid` are mutually exclusive.
***
# Mesa CLI Configuration
Source: https://docs.mesa.dev/content/reference/mesa-cli-configuration
Environment variables and options for the Mesa CLI.
`mesa` is configured through environment variables.
## Access token
| Variable | Description |
| ------------------- | ------------------------------------------ |
| `MESA_ACCESS_TOKEN` | Short-lived access token for CLI commands. |
Set `MESA_ACCESS_TOKEN` to a scoped token you minted on trusted infrastructure, and keep the private key out of the CLI's environment.
```bash theme={null}
MESA_ACCESS_TOKEN=eyJ... mesa mount --layout layout.json --daemonize
```
The CLI derives the organization from the token's issuer. Use one access token
and organization per CLI process.
## Optional
| Variable | Description |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| `MESA_MOUNT_POINT` | Path where repos are mounted (default: `~/.local/share/mesa/mnt`). |
| `MESA_MESAIGNORE_PATH` | Path to a global `.mesaignore` file. See [.mesaignore](/content/mesafs/advanced/mesaignore). |
| `MESA_CACHE_ENABLED` | Enable the disk cache (`true`/`false`, default: `true`). |
| `MESA_CACHE_MAX_SIZE` | Maximum disk cache size (e.g. `1GB`). |
| `MESA_CACHE_MAX_MEMORY_SIZE` | Maximum in-memory cache size (e.g. `256MB`). |
| `MESA_CACHE_PATH` | Path for the disk cache. |
| `MESA_DAEMON_LOG_FILE` | Daemon log path. Defaults to `/tmp/mesa-/mesa.log`; falls back to stdout if the file cannot be opened. |
| `MESA_DAEMON_LOG_COLOR` | Log color mode: `auto`, `always`, or `never`. |
| `MESA_PREFETCH_ENABLED` | Enable speculative prefetching (`true`/`false`). |
| `MESA_PREFETCH_MAX_DEPTH` | Maximum directory depth the prefetcher walks. |
| `MESA_PREFETCH_MAX_CONCURRENCY` | Maximum concurrent prefetch requests. |
| `MESA_PREFETCH_CACHE_PRESSURE_LIMIT` | Fraction of cache budget at which prefetching backs off (`0.0`–`1.0`). |
| `MESA_TELEMETRY` | Enable telemetry (`true`/`false`). |
| `MESA_TELEMETRY_COLLECTOR_URL` | OpenTelemetry collector URL for trace export. |
| `MESA_RUNTIME_DIR` | Directory for runtime state (PID file, RPC socket). |
| `MESA_SERVICE_DOMAIN` | Custom service domain for self-hosted Mesa. |
| `MESA_UID` / `MESA_GID` | POSIX owner of the mounted filesystem. |
## Daemon mode
Run `mesa mount` as a background daemon:
```bash theme={null}
mesa mount --layout layout.json --daemonize
```
Log output goes to stdout by default. Redirect it to a file with
`MESA_DAEMON_LOG_FILE`:
```bash theme={null}
MESA_DAEMON_LOG_FILE=/var/log/mesa.log mesa mount --layout layout.json --daemonize
```
## Ignore rules
Set `MESA_MESAIGNORE_PATH` to a `.mesaignore` file to control which files are
kept locally vs. uploaded to VCS. See
[.mesaignore](/content/mesafs/advanced/mesaignore) for details.
# bookmarks.create()
Source: https://docs.mesa.dev/content/reference/py/bookmarks-create
Create a bookmark at an existing change.
Create a bookmark that points to an existing change ID.
**Required scope: `write`**
```python theme={null}
bookmark = await mesa.bookmarks.create(
repo="app",
name="feature-x",
change_id="zyxwvutsrqponmlkzyxwvutsrqponmlk",
)
```
## Options
Repository name.
Bookmark name to create.
Existing change ID the bookmark should point to.
## Response
Bookmark name.
Change ID the bookmark points to.
Whether this is the repository default bookmark.
# bookmarks.delete()
Source: https://docs.mesa.dev/content/reference/py/bookmarks-delete
Delete a bookmark from a repository.
Delete a bookmark.
**Required scope: `write`**
```python theme={null}
result = await mesa.bookmarks.delete(repo="app", bookmark="feature-x")
print(result.success)
```
## Options
Repository name.
Bookmark name to delete.
## Response
Whether the operation succeeded.
# bookmarks.list()
Source: https://docs.mesa.dev/content/reference/py/bookmarks-list
List bookmarks in a repository.
Bookmarks are named refs analogous to Git branches.
**Required scope: `read`**
```python theme={null}
bookmarks = await mesa.bookmarks.list(repo="app")
for bookmark in bookmarks.bookmarks:
print(bookmark.name, bookmark.change_id, bookmark.is_default)
```
## Options
Repository name.
Opaque pagination cursor from a previous response.
Maximum number of bookmarks to return. The server maximum is `100`.
## Response
Cursor for the next page, or null when no more results remain.
Whether another page of results is available.
Bookmark objects.
### Bookmark
Bookmark name.
Change ID the bookmark points to.
Whether this is the repository default bookmark.
# bookmarks.merge()
Source: https://docs.mesa.dev/content/reference/py/bookmarks-merge
Merge one bookmark into another.
Merge `source` into `target`. The target bookmark advances to the merge result.
**Required scope: `write`**
```python theme={null}
result = await mesa.bookmarks.merge(
repo="app",
source="feature-x",
target="main",
authors=[{"name": "Docs Bot", "email": "docs@example.com"}],
delete_source=True,
)
print(result.merge_type, result.change_id)
```
Resolve conflicts on retry:
```python theme={null}
from mesa_sdk import WholeFileResolution
result = await mesa.bookmarks.merge(
repo="app",
source="feature-x",
target="main",
authors=[{"name": "Docs Bot", "email": "docs@example.com"}],
resolutions=[WholeFileResolution(path="README.md", take="source")],
)
```
## Options
Repository name.
Source bookmark with changes to merge.
Target bookmark to advance.
Delete the source bookmark after a successful merge.
When `True`, accept a merge that still contains textual conflicts. When omitted or `False`, conflicted merges fail with `ConflictError`.
Whole-file or per-hunk conflict resolutions applied before conflict detection.
Commit authors, in order, with at least one entry. Private-key clients set this.
## Response
Type of merge that was performed.
Whether the resulting merge change still contains unresolved conflicts.
Commit OID the target bookmark now points to.
Change ID the target bookmark points to after the merge.
Bookmark that was merged into.
Bookmark that was merged from.
Whether the source bookmark was deleted after the merge.
# bookmarks.move()
Source: https://docs.mesa.dev/content/reference/py/bookmarks-move
Move a bookmark to a different change.
Repoint an existing bookmark to a different change ID. Moves must advance
history unless `allow_backwards=True` is provided.
**Required scope: `write`**
```python theme={null}
bookmark = await mesa.bookmarks.move(
repo="app",
bookmark="main",
change_id="zyxwvutsrqponmlkzyxwvutsrqponmlk",
)
print(bookmark.from_change_id, bookmark.change_id)
```
## Options
Repository name.
Bookmark name to move.
Change ID to move the bookmark to.
Allow an intentional backward or sideways move.
## Response
Bookmark name.
Change ID the bookmark points to.
Whether this is the repository default bookmark.
Previous change ID before the move, or null when unavailable.
# changes.create()
Source: https://docs.mesa.dev/content/reference/py/changes-create
Create a change on top of an existing base change.
Create a new change, optionally applying initial file operations atomically. File content must be base64-encoded.
**Required scope: `write`**
```python theme={null}
import base64
from mesa_sdk import FileDelete, FileUpsert
change = await mesa.changes.create(
repo="app",
base_change_id="zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
message="Add README",
authors=[{"name": "Docs Bot", "email": "docs@example.com"}],
files=[
FileUpsert(
path="README.md",
content=base64.b64encode(b"# App\n").decode(),
),
FileDelete(path="old.txt"),
],
)
```
## Options
Repository name.
Change to create the new change on top of. Use Mesa's virtual root change ID for the first change in an empty repository.
Change description. Required when `files` is non-empty. When `files` is omitted, pass `None` to omit the field and create the change with no description, or pass `""` explicitly.
Commit authors, in order, with at least one entry. Private-key clients set this.
Committer identity. When omitted, the first author is used.
File operations to apply atomically. Entries are `FileUpsert` or `FileDelete`.
## Response
Change ID.
Current commit OID for the change.
Whether the current commit still contains unresolved conflicts.
Current change description. Empty string means the change has no description.
Commit author identity.
Committer identity.
Parent change IDs for this change.
Creation time.
Last update time.
### CommitIdentity
Author or committer display name.
Author or committer email address.
Timestamp for this identity when provided.
# changes.get()
Source: https://docs.mesa.dev/content/reference/py/changes-get
Fetch a specific change by ID.
Fetch one change and its file/conflict summary.
**Required scope: `read`**
```python theme={null}
change = await mesa.changes.get(
repo="app",
change_id="zyxwvutsrqponmlkzyxwvutsrqponmlk",
)
print(change.files)
print(change.conflicts)
```
## Options
Repository name.
Mesa change ID.
## Response
Change ID.
Current commit OID for the change.
Whether the current commit still contains unresolved conflicts.
Current change description. Empty string means the change has no description.
Complete ordered commit attribution. Every entry is an equal contributor.
Shared authored timestamp. The virtual root uses the Unix epoch.
Committer identity. The virtual root has an empty name and email at the Unix epoch.
The deprecated REST `author` field is not returned by the SDK; read `authors` and `authored_at`.
Parent change IDs for this change.
Creation time.
Last update time.
For Mesa's virtual root, `authors` is `[]` and the signature fields are empty at the Unix epoch. Check `len(authors)` to detect the root.
Repository-relative paths changed by this change.
Repository-relative paths that still contain unresolved conflicts.
### CommitIdentity
Author display name.
Author email address, or `None` when no email was supplied.
### CommitSignature
Committer display name.
Email address, or `""` when none was supplied. This string form is retained for compatibility.
Timestamp for this signature when provided.
# changes.list()
Source: https://docs.mesa.dev/content/reference/py/changes-list
List reachable changes for a repository.
List current reachable changes, optionally restricted to a bookmark.
**Required scope: `write`**
```python theme={null}
changes = await mesa.changes.list(repo="app", bookmark="main")
for change in changes.changes:
print(change.id, change.current_commit_oid, change.message)
```
## Options
Repository name.
Opaque pagination cursor from a previous response.
Maximum number of changes to return. The server maximum is `100`.
Restrict results to changes reachable from this bookmark.
## Response
Cursor for the next page, or null when no more results remain.
Whether another page of results is available.
Change objects.
### Change
Change ID.
Current commit OID for the change.
Whether the current commit still contains unresolved conflicts.
Current change description. Empty string means the change has no description.
Complete ordered commit attribution. Every entry is an equal contributor.
Shared authored timestamp. The virtual root uses the Unix epoch.
Committer identity. The virtual root has an empty name and email at the Unix epoch.
The deprecated REST `author` field is not returned by the SDK; read `authors` and `authored_at`.
Parent change IDs for this change.
Creation time.
Last update time.
### CommitIdentity
Author display name.
Author email address, or `None` when no email was supplied.
### CommitSignature
Committer display name.
Email address, or `""` when none was supplied. This string form is retained for compatibility.
Timestamp for this signature when provided.
# changes.patch()
Source: https://docs.mesa.dev/content/reference/py/changes-patch
Amend metadata, apply file operations, or resolve conflicts on an existing change.
Patch a change. A successful patch snapshots the result into a new commit owned by the same Mesa change.
**Required scope: `write`**
```python theme={null}
import base64
from mesa_sdk import FileUpsert
change = await mesa.changes.patch(
repo="app",
change_id="zyxwvutsrqponmlkzyxwvutsrqponmlk",
message="Update README",
authors=[{"name": "Docs Bot", "email": "docs@example.com"}],
files=[
FileUpsert(
path="README.md",
content=base64.b64encode(b"# Updated\n").decode(),
)
],
)
```
A patch that applies conflict resolutions keeps the existing commit authors, so omit `authors`.
Resolve conflicts:
```python theme={null}
from mesa_sdk import HunkFix, HunkResolution
change = await mesa.changes.patch(
repo="app",
change_id="zyxwvutsrqponmlkzyxwvutsrqponmlk",
resolutions=[
HunkResolution(
path="README.md",
hunks=[HunkFix(hunk_id="h1", take="target")],
)
],
)
```
## Options
Repository name.
Existing Mesa change ID.
Replacement change description. Pass `None` to omit the field and preserve the existing description; pass `""` to clear it.
Commit authors, in order, with at least one entry. Private-key clients set this, but omit it when you apply `resolutions`.
New committer identity.
File operations to apply. Mutually exclusive with `resolutions`.
Optimistic-concurrency token. When set, the patch fails with `ConflictError` if the change has advanced past this commit.
Conflict resolutions to apply to a conflicted change. Mutually exclusive with `files`.
## Response
Change ID.
Current commit OID for the change.
Whether the current commit still contains unresolved conflicts.
Current change description. Empty string means the change has no description.
Commit author identity.
Committer identity.
Parent change IDs for this change.
Creation time.
Last update time.
### CommitIdentity
Author or committer display name.
Author or committer email address.
Timestamp for this identity when provided.
# content.get()
Source: https://docs.mesa.dev/content/reference/py/content-get
Read a file, symlink, or directory listing from a repository.
Read repository content without mounting the filesystem.
**Required scope: `read`**
```python theme={null}
root = await mesa.content.get(repo="app", depth=1)
print(root.type)
print(root.is_dir())
readme = await mesa.content.get(repo="app", path="README.md")
if readme.is_file():
print(readme.content) # base64-encoded bytes
```
## Options
Repository name.
Change ID to read from. Defaults to the current change at the repository's default bookmark tip.
Repository-relative path. Omit for the repository root.
Directory traversal depth. `0` returns directory metadata only, `1` returns direct children, and the server maximum is `10`.
## Response
The method returns one generated model variant. The generated `type_` field
remains available, and `type` is exposed as a read-only alias.
Returns `True` when the response is a regular file.
Returns `True` when the response is a directory.
Returns `True` when the response is a symlink.
Regular file content.
Symbolic link content.
Directory listing.
### ContentFile
Content variant discriminator.
Base name of the file.
Repository-relative path.
Git object SHA.
Whether this path is conflicted when known.
Size in bytes.
Encoding used for `content`.
Base64-encoded file bytes.
POSIX file mode.
Extended attributes for this path. Values are base64-encoded.
### ContentSymlink
Content variant discriminator.
Base name of the symlink.
Repository-relative path.
Git object SHA.
Whether this path is conflicted when known.
Size in bytes.
Encoding used for `content`.
Base64-encoded symlink target bytes.
POSIX symlink mode.
Extended attributes for this path. Values are base64-encoded.
### ContentDirectory
Content variant discriminator.
Base name of the directory.
Repository-relative path.
Git object SHA.
Number of direct children in this directory.
Directory entries returned for the requested depth.
Extended attributes attached to this directory. Values are base64-encoded.
### ContentDirectoryEntry
Entry variant discriminator.
Base name of the entry.
Repository-relative path.
Git object SHA.
Size in bytes for file and symlink entries.
POSIX mode for file and symlink entries.
# diffs.get()
Source: https://docs.mesa.dev/content/reference/py/diffs-get
Inspect the diff between two changes.
Retrieve a structured diff between two Mesa change IDs.
**Required scope: `read`**
```python theme={null}
diff = await mesa.diffs.get(
repo="app",
base_change_id="zyxwvutsrqponmlkzyxwvutsrqponmlk",
head_change_id="lmnopqrstuvwzyxklmnopqrstuvwzyxk",
)
print(diff.stats.additions, diff.stats.deletions)
```
Fetch only conflicted entries:
```python theme={null}
diff = await mesa.diffs.get(
repo="app",
base_change_id="zyxwvutsrqponmlkzyxwvutsrqponmlk",
head_change_id="lmnopqrstuvwzyxklmnopqrstuvwzyxk",
conflicts="only",
)
```
## Options
Repository name.
Base Mesa change ID.
Head Mesa change ID.
Controls whether conflicted entries are included, excluded, or returned exclusively.
## Response
`entries[]` and `conflicted_entries[]` are mutually exclusive: unresolved conflicted paths appear only in `conflicted_entries[]`.
Base change used for comparison.
Head change used for comparison.
Aggregate counts for the response.
Whether the response hit the server entry limit and is incomplete.
Structural changed entries. Unchanged paths are omitted.
Conflict-only details for unresolved paths.
### DiffStats
Number of structural entries returned in `entries[]`.
Total added lines across textual diff hunks.
Total deleted lines across textual diff hunks.
Sum of `additions` and `deletions`.
Number of paths returned in `conflicted_entries[]`.
Total conflict hunk count across `conflicted_entries[]`.
### DiffEntry
Repository-relative path at the head change.
How the entry changed between the base and head changes.
Previous repository-relative path for renamed entries. Null for non-renames.
Approximate size in bytes of the changed entry content, not the textual diff.
Always false for entries returned in `entries[]`. Conflicted paths are returned in `conflicted_entries[]`.
Why textual hunk data is unavailable. Null when `hunks` is present.
Structured textual diff hunks. Null when `omitted_reason` is set.
Structured conflict hunks for the entry, or null when there are none. Structural entries in `entries[]` normally return null.
### DiffHunk
Starting line number in the base side.
Number of base-side lines covered by the hunk.
Starting line number in the head side.
Number of head-side lines covered by the hunk.
Lines in this textual diff hunk.
### DiffLine
Line classification inside a textual diff hunk.
Line text.
### ConflictedDiffEntry
Repository-relative path for the unresolved conflict.
Per-hunk conflict detail. Empty when `omitted_reason` is set or the conflict is non-textual.
Approximate size in bytes of the largest side of the conflict.
Why hunk data is unavailable. When set, fetch file bytes with `mesa.content.get()` against the target or source change to compose a whole-file resolution.
### DiffConflictHunk
Stable identifier for the conflict hunk.
Starting line number in the base side.
Number of base-side lines covered by the hunk.
Starting line number in the head side.
Number of head-side lines covered by the hunk.
Target side of the conflicted hunk.
Base side of the conflicted hunk.
Source side of the conflicted hunk.
### ChangeConflictHunkSide
Base64-encoded raw bytes for this side of the conflicted hunk.
# fs.bash()
Source: https://docs.mesa.dev/content/reference/py/fs-bash
Run shell commands against a mounted Mesa filesystem.
`fs.bash(...)` builds a Bash interpreter rooted at the mounted filesystem. Commands run against MesaFS; no host shell is spawned.
```python theme={null}
bash = fs.bash(cwd="/workspace", env={"CI": "true"}, timeout_ms=30_000)
result = await bash.exec("rg -n TODO src | head -20")
print(result.exit_code)
print(result.stdout.decode())
print(result.stderr.decode())
```
## Options
Environment variables for the shell. Defaults to empty. The host process environment is not inherited.
Working directory inside the mount. Defaults to `/`.
Per-exec wall-clock timeout in milliseconds. Defaults to 30 seconds.
## exec()
`exec(commands)` takes one string containing one or more shell statements.
```python theme={null}
result = await bash.exec("""
rg -l integration .
python -m pytest
""")
```
Shell script to execute.
## Response
`exec(...)` returns `ExecResult`.
Standard output bytes.
Standard error bytes.
Process exit code. `0` indicates success.
## Command failures
Command failures usually return a non-zero `exit_code`; they do not automatically raise.
```python theme={null}
result = await bash.exec("cat /workspace/missing.txt")
if result.exit_code != 0:
print(result.stderr.decode())
```
Timeouts may surface as either an exception or a non-zero exit depending on where execution is interrupted.
## Binary files
Use `fs.read(...)` for binary files. Text-oriented shell commands such as `cat` are intended for text output.
# fs.bookmarks
Source: https://docs.mesa.dev/content/reference/py/fs-bookmarks
Manage bookmarks from a mounted Mesa filesystem.
`fs.bookmarks` exposes bookmark operations that are local to a mounted filesystem session.
```python theme={null}
from mesa_sdk import repo
async with mesa.fs(
layout={"/workspace": repo("app", mode="rw")},
authors=[{"name": "Workspace Agent", "email": "agent@example.com"}],
).mount() as fs:
bookmarks = await fs.bookmarks.list("app")
await fs.bookmarks.create("app", "feature-x")
await fs.bookmarks.move("app", "feature-x", change_id="zyxwvutsrqponmlkzyxwvutsrqponmlk")
```
## Methods
Return every bookmark name on a mounted repository.
Create a bookmark at the active change's commit.
Move a bookmark to a change. The move must advance history unless
`allow_backwards=True` is provided.
## Errors
`create(...)` raises `FileExistsError` when the bookmark already exists.
`move(...)` rejects backward or sideways moves unless `allow_backwards=True`.
Use `mesa.bookmarks` for bookmark operations through the REST API, including
moving, merging, and deleting bookmarks.
# fs.changes
Source: https://docs.mesa.dev/content/reference/py/fs-changes
Manage active changes on a mounted Mesa filesystem.
`fs.changes` operates on the mounted filesystem checkout. For REST-level change creation and patching, use `mesa.changes`.
```python theme={null}
from mesa_sdk import repo
async with mesa.fs(
layout={"/workspace": repo("app", mode="rw")},
authors=[{"name": "Workspace Agent", "email": "agent@example.com"}],
).mount() as fs:
change_id = await fs.changes.new("app", bookmark="main", message="Cool new feature")
await fs.write("/workspace/feature.py", b"print('hi')\n")
current = await fs.changes.current("app")
```
## Methods
Create a new change forked from a bookmark or existing change. Exactly one of `bookmark` or `change_id` must be supplied. Pass `message` to set the new change description; `None` omits it and creates the change with no description.
Check out an existing change. This never creates a new change. Exactly one of `bookmark` or `change_id` must be supplied.
List changes reachable from the current checkout, most recent first.
Return the currently active change for a mounted repo.
## ChangeInfo
Mesa change ID.
Git commit OID for the change snapshot.
## Errors
Raises `FileNotFoundError` if the source bookmark or change does not exist.
# mesa.fs()
Source: https://docs.mesa.dev/content/reference/py/fs-mount
Define a layout, mount it as a Mesa virtual filesystem, or mint its token.
`mesa.fs(layout=..., ttl=..., authors=...)` builds a `FilesystemDefinition`: a raw `Layout`, its authors, and its token lifetime, bundled with `layout()`, `mount()`, and `token()`. Calling `.mount()` is an async context manager that yields a `MesaFileSystem`.
```python theme={null}
import os
from mesa_sdk import Mesa, repo
mesa = Mesa(private_key=os.environ["MESA_PRIVATE_KEY"])
async with mesa.fs(
layout={"/workspace": repo("app", mode="rw", at={"bookmark": "main"})},
authors=[{"name": "Mesa Bot", "email": "mesa-bot@example.com"}],
).mount() as fs:
data = await fs.read("/workspace/README.md")
print(data.decode())
```
Map `repos.list()` results into layout declarations when the repository set is dynamic:
```python theme={null}
result = await mesa.repos.list(
tags={"$and": [{"environment": "prod"}, {"workload": {"$in": ["sync", "index"]}}]}
)
async with mesa.fs(
layout={"/skills": [repo(r.name, mode="ro") for r in result.repos]},
authors=[{"name": "Mesa Bot", "email": "mesa-bot@example.com"}],
).mount() as fs:
...
```
The private-key client signs a short-lived, layout-scoped token locally for the mount. The mount keeps that token for its whole lifetime; nothing refreshes it in the background.
See [Layouts](/content/mesafs/layouts) for nesting, structural rules, and running a layout mount in a sandbox.
## `mesa.fs(...)`
Synchronous. Builds the definition eagerly so invalid layouts raise at this call.
Map of absolute mount paths (`/`-prefixed) to one layout child. The mount contains exactly this visible path tree.
Lifetime, in seconds, of every token the definition mints, through `token()` and under the hood in `mount()`. Defaults to `900` and allows up to `14400`.
Commit authors, in order, with at least one entry.
### Definition members
An independent plain-dict snapshot of the validated layout. Pass it to `json.dumps(...)` to produce the document `mesa mount --layout` reads.
Mount the layout as the complete visible path tree. Accepts only runtime options (`disk_cache`). The mount's token lifetime is the definition's `ttl`.
Mint the layout-scoped, least-privilege access token. Each repository receives read only access for `mode="ro"` or read and write access for `mode="rw"`.
`mesa.fs(...)` validates the layout structurally before returning the definition. Repository names resolve only at mount time — a layout naming a nonexistent repository still produces a definition and token, then fails when the mount resolves it.
```python theme={null}
import json
from mesa_sdk import repo
definition = mesa.fs(
layout={"/workspace": repo("app", mode="rw")},
authors=[{"name": "Mesa Bot", "email": "mesa-bot@example.com"}],
ttl=3600,
)
sandbox.write_file("layout.json", json.dumps(definition.layout(), indent=2))
token = (await definition.token()).token
```
## `repo(selector, *, mode, ...)`
Declare one repository in a layout. Do not construct `Repo` mappings by hand.
```python theme={null}
repo("app", mode="rw", at={"bookmark": "main"})
repo({"name": "app"}, mode="ro", at={"change_id": "zyxwvutsrqponmlkzyxwvutsrqponmlk"})
repo(
"app",
mode="rw",
branched_from={
"bookmark": "main",
"as": {"bookmark": "run-27", "describe": "initiate plan..."},
},
)
```
Repository name within the client's organization, as a string or `{"name": "..."}`.
Access mode. Always required — there is no default. `"ro"` rejects writes with `EROFS`.
Pin an existing revision: `{"bookmark": ...}` or `{"change_id": ...}`. Mutually exclusive with `branched_from`. When neither `at` nor `branched_from` is set, the repository's default bookmark is used.
Fork a new empty descendant from a parent tip at cold open and check it out. Requires `mode="rw"`. Shape: `{"bookmark"|"change_id": ..., "as": {"bookmark"?: str, "describe"?: str}}`. Omit `as` (or `as.bookmark`) for an anonymous tip. Parent bookmark is not moved. Mutually exclusive with `at`.
Directory-name override. Only valid when the declaration is an element of a sequence value in the layout.
Nested repository mounts declared beneath this repository's mount path. Keys are relative paths.
## Runtime mount options
Non-token options accepted by `definition.mount(...)`.
### DiskCacheConfig
```python theme={null}
from mesa_sdk import DiskCacheConfig
async with mesa.fs(
layout={"/workspace": repo("app", mode="rw")},
authors=[{"name": "Mesa Bot"}],
).mount(
disk_cache=DiskCacheConfig(path="/tmp/mesa-cache", max_size_bytes=500_000_000),
) as fs:
...
```
Directory for the on-disk cache.
Optional cache size cap. When omitted, the native extension auto-sizes the budget against system resources.
## Paths
Mounted paths are whatever the layout declares:
```text theme={null}
/workspace/README.md
/workspace/src/main.py
/skills/code-review/SKILL.md
```
## Response
`definition.mount()` yields a `MesaFileSystem`.
## MesaFileSystem
The yielded `fs` object exposes async file I/O, metadata, traversal, mutation, Bash, and mounted-repo version-control helpers.
```python theme={null}
async with mesa.fs(
layout={"/workspace": repo("app", mode="rw")},
authors=[{"name": "Mesa Bot"}],
).mount() as fs:
await fs.mkdir("/workspace/src", recursive=True)
await fs.write("/workspace/src/main.py", b"print('hello')\n")
data = await fs.read("/workspace/src/main.py")
```
### Byte I/O
Read a file as bytes.
Replace file contents, creating the file if missing. Parent directories must already exist.
Append bytes to a file, creating it if missing.
Return whether a path exists. Follows symlinks.
### Metadata and traversal
Return metadata for a path, following symlinks.
Return metadata for a path without following symlinks.
Return entry names in a directory. Sort client-side if you need deterministic ordering.
Resolve symlinks and `..` segments to a canonical path.
Return the target of a symlink.
Join and normalize a path against a base path without touching the filesystem.
### Mutations
Create a directory. With `recursive=True`, create missing parents and do nothing when the path already exists as a directory.
Remove a file or directory. Use `recursive=True` for non-empty directories and `force=True` to ignore missing paths.
Copy a file or directory. Use `recursive=True` for directories.
Move or rename a file or directory.
Set permission bits, such as `0o755`.
Create a symlink. Relative targets are stored verbatim and resolve against the parent of `link` at read time.
Set access and modification times. Values are milliseconds since the Unix epoch, not seconds.
Hard links are not supported and this method raises `NotImplementedError`.
### Subscriptions
MesaFS reads and writes are realtime by default. Use subscriptions only when your process needs an event stream that identifies which paths changed, such as to refetch data and rerender a frontend.
Subscribe to filesystem invalidation events. The handler is called after the changed state is visible through this filesystem instance.
```python theme={null}
async def on_change(event):
if not event.recursive:
content = await fs.read(event.path)
print(event.path, content.decode())
subscription = fs.subscribe(on_change)
await subscription.unsubscribe()
```
Callback invoked for each filesystem invalidation.
Absolute MesaFS path that changed, such as `/workspace/src/index.py`.
Whether descendants of `path` may have changed. Refresh any cached directory or subtree state below `path` when this is `True`.
Stop receiving events and close the underlying watcher.
### FsStat
`stat(...)` and `lstat(...)` return `FsStat`.
Whether the path is a regular file.
Whether the path is a directory.
Whether the path is a symlink. This is `False` from `stat(...)` when the target exists because `stat` follows symlinks.
POSIX mode bits.
Size in bytes.
Modification time in milliseconds since the Unix epoch.
### Related filesystem methods
| Method | Reference |
| ------------------- | ------------------------------------------------------ |
| `fs.bash(...)` | [fs.bash()](/content/reference/py/fs-bash) |
| `fs.changes` | [fs.changes](/content/reference/py/fs-changes) |
| `fs.bookmarks` | [fs.bookmarks](/content/reference/py/fs-bookmarks) |
| `fs.subscribe(...)` | [Advanced Realtime](/content/mesafs/advanced/realtime) |
## Errors
Raises `InvalidOptionsError` for a missing or empty layout, a `ttl` outside `1..14400`, missing or rejected `authors`, or invalid `mode`. A layout that breaks the [structural rules](/content/mesafs/layouts#declaration-fields) raises at the `mesa.fs(...)` call rather than at mount. Token signing or VCS connection failures can raise `ApiError` subclasses or connection errors.
## Multiprocessing
MesaFS is not fork-safe. If you use `multiprocessing`, set the start method to `spawn` or `forkserver` before creating Mesa objects.
```python theme={null}
import multiprocessing
multiprocessing.set_start_method("spawn")
```
# Overview
Source: https://docs.mesa.dev/content/reference/py/index
The Mesa Python SDK is the ergonomic async client for Mesa. It wraps the generated `mesa-rest` client, resolves the default organization for you, and exposes a native virtual filesystem for repo I/O and shell execution.
Python 3.10 or newer is required.
## Installation
```bash theme={null}
pip install mesa-sdk
```
## Create a client
```python theme={null}
import asyncio
import os
from mesa_sdk import Mesa
mesa = Mesa(private_key=os.environ["MESA_PRIVATE_KEY"])
async def main():
repos = await mesa.repos.list()
print(f"found {len(repos.repos)} repos")
asyncio.run(main())
```
Set `MESA_PRIVATE_KEY` in a trusted environment to omit `private_key` from the constructor.
## Client options
```python theme={null}
from mesa_sdk import Mesa
mesa = Mesa(
private_key="mesa_private_key_acme_...",
api_url="https://api.mesa.dev/v1",
user_agent="my-app/1.0.0",
)
```
Ed25519 private key. The SDK reads `MESA_PRIVATE_KEY` when no explicit private key is supplied.
REST API base URL. Defaults to `https://api.mesa.dev/v1`. `http` and `https` are accepted. Trailing slashes are stripped.
Appended to the SDK user agent. The default user agent starts with `mesa-sdk-python`.
## Client lifecycle
Create one `Mesa` client for your process or application and reuse it across request handlers. If your framework has a lifespan hook and you want explicit cleanup, wrap the client in `async with Mesa(...)` at application lifespan, not inside each handler.
```python theme={null}
from mesa_sdk import Mesa
mesa = Mesa()
async def handler():
repos = await mesa.repos.list()
return repos
```
## Organization resolution
The client reads its organization from the private key. Resource methods always use that organization and do not accept an `org` value.
```python theme={null}
print(mesa.org.slug)
await mesa.repos.list()
```
Use `await mesa.org.get()` when you need organization metadata from the API.
## Resource APIs
| API | Purpose |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mesa.repos` | Create, read, update, delete, sync upstreams, and read upstream sync history on repositories. |
| `mesa.content` | Read files, symlinks, and directory listings without mounting. |
| `mesa.changes` | Create, patch, and inspect Mesa changes. |
| `mesa.diffs` | Inspect diffs and conflicts between changes. |
| `mesa.bookmarks` | Manage branch-like bookmark refs. |
| `mesa.webhook_targets` | Manage outbound webhook targets. |
| `mesa.fs` | Define a layout and mount it as a virtual filesystem, or mint its scoped token. Call `mesa.fs(layout=..., authors=..., ttl=...)` then `.mount()` / `.token()`. |
| `mesa.org` | Read the private key's organization slug with `mesa.org.slug`, or fetch organization metadata with `await mesa.org.get()`. |
## Response objects
The high-level SDK returns model instances generated by `mesa-rest`. Use attribute access, not dictionary access.
```python theme={null}
repos = await mesa.repos.list()
for repo in repos.repos:
print(repo.name, repo.head_change_id)
```
## Common types
Import common dataclasses and native result types from `mesa_sdk`.
```python theme={null}
import base64
from mesa_sdk import Author, Committer, FileDelete, FileUpsert, Layout
files = [
FileUpsert(path="README.md", content=base64.b64encode(b"hello").decode()),
FileDelete(path="old.txt"),
]
authors: list[Author] = [{"name": "Build Bot"}]
committer = Committer(name="Build Bot", email="build@example.com")
```
| Type | Purpose |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Author` | Commit attribution for commit-producing operations. |
| `Committer` | Committer identity for change creation and updates. |
| `FileUpsert` | Create or replace one file in a change. `content` must be base64-encoded. |
| `FileDelete` | Delete one file in a change. |
| `WholeFileResolution` | Resolve a conflicted path by replacing full content or taking one side. |
| `HunkResolution` / `HunkFix` | Resolve individual conflict hunks. |
| `FilesystemDefinition` | Layout, authors, and token lifetime bundled with the `layout()`, `mount()`, and `token()` operations they configure, returned by `mesa.fs(layout=..., authors=...)`. |
| `AccessToken` | Short-lived access token returned by `definition.token()`, scoped to the layout that minted it, with `token` and timezone-aware `expires_at` fields. |
| `Layout` | Raw path map validated by `mesa.fs(...)` and returned by `definition.layout()`. Serialize it for the CLI with `json.dumps(...)`. |
| `repo` | Helper that builds one layout repository declaration (`mode` required). |
| `DiskCacheConfig` | Configure on-disk MesaFS cache placement and size (passed to `definition.mount(...)`). |
| `FsStat` | File metadata returned by `stat` and `lstat`. |
| `ChangeInfo` | Change metadata returned by mounted filesystem change operations. |
| `ExecResult` | Output from `fs.bash().exec(...)`. |
Upstream configuration types live in `mesa_sdk.types`:
```python theme={null}
from mesa_sdk.types import TokenAuth, UpstreamConfig, UsernamePasswordAuth
public = UpstreamConfig(url="https://github.com/acme/app.git")
token = UpstreamConfig(
url="https://github.com/acme/app.git",
auth=TokenAuth(token="github_pat_...", token_username="bot"),
)
password = UpstreamConfig(
url="https://git.example.com/acme/app.git",
auth=UsernamePasswordAuth(username="bot", password="secret"),
)
```
On `mesa.repos.update(...)`, `UpstreamConfig.auth` is tri-state: omit to preserve existing credentials, pass `None` to clear credentials, or pass `TokenAuth` / `UsernamePasswordAuth` to set credentials.
## Error model
REST API operations raise `MesaError` subclasses.
| Exception | Status | Meaning |
| --------------------- | ------------ | ----------------------------------------------------------------------- |
| `ValidationError` | `400`, `406` | Invalid request parameters or unacceptable response variant. |
| `AuthenticationError` | `401` | Missing or invalid access token. |
| `AuthorizationError` | `403` | The access token does not have the required scope or repository access. |
| `NotFoundError` | `404` | Requested resource does not exist. |
| `ConflictError` | `409` | Resource conflict, optimistic concurrency failure, or merge conflict. |
| `RateLimitError` | `429` | Rate limit exceeded. |
| `ServerError` | `5xx` | Server-side failure. |
SDK setup errors include `MissingCredentialError`, `InvalidApiUrlError`, `OrgResolutionError`, and `InvalidOptionsError`.
Filesystem and Bash operations raise built-in Python exceptions such as `FileNotFoundError`, `FileExistsError`, `IsADirectoryError`, `NotADirectoryError`, `PermissionError`, `NotImplementedError`, and `OSError`.
## Raw generated client
`mesa.raw` exposes the authenticated generated `mesa-rest` client. Use it when the high-level SDK does not expose a generated REST operation or option yet.
```python theme={null}
from mesa_rest.api.repo import list_repos
response = await list_repos.asyncio_detailed("acme", client=mesa.raw)
if response.status_code == 200:
print(response.parsed.repos)
```
Raw generated calls return a `Response[T]` wrapper with `status_code`, `parsed`, and `headers`. High-level SDK methods unwrap successful responses and raise typed errors for non-2xx responses.
## Complete example
```python theme={null}
import asyncio
import base64
import os
from mesa_sdk import FileUpsert, Mesa, repo
mesa = Mesa(private_key=os.environ["MESA_PRIVATE_KEY"])
async def main():
created = await mesa.repos.create(name="demo")
change = await mesa.changes.create(
repo=created.name,
base_change_id=created.head_change_id,
message="Add README",
authors=[{"name": "Docs Bot", "email": "docs@example.com"}],
files=[
FileUpsert(
path="README.md",
content=base64.b64encode(b"# Demo\n").decode(),
)
],
)
await mesa.bookmarks.move(
repo=created.name,
bookmark=created.default_bookmark,
change_id=change.id,
)
async with mesa.fs(
layout={"/workspace": repo(created.name, mode="rw")},
authors=[{"name": "Docs Bot", "email": "docs@example.com"}],
).mount() as fs:
data = await fs.read("/workspace/README.md")
print(data.decode())
asyncio.run(main())
```
# org.get()
Source: https://docs.mesa.dev/content/reference/py/org-get
Fetch organization metadata.
Use `mesa.org.get()` to fetch metadata for the organization encoded in the client's private key.
**Required scope: `read`**
```python theme={null}
org = await mesa.org.get()
print(org.slug)
```
## Response
Organization creation time.
Number of repositories in the organization.
# repos.create()
Source: https://docs.mesa.dev/content/reference/py/repos-create
Create a Mesa repository, optionally with a default bookmark or upstream remote.
Create a repository in the organization encoded in the client's private key.
**Required scope: `write`**
```python theme={null}
repo = await mesa.repos.create(name="app")
print(repo.name)
print(repo.head_change_id)
```
Create with an upstream Git remote:
```python theme={null}
from mesa_sdk.types import TokenAuth, UpstreamConfig
repo = await mesa.repos.create(
name="app",
default_bookmark="main",
upstream=UpstreamConfig(
url="https://github.com/acme/app.git",
auth=TokenAuth(token="github_pat_...", token_username="bot"),
),
)
```
## Options
Repository name. If omitted, the API generates a name.
Default bookmark name. Defaults to `main` when omitted.
Optional upstream Git remote to attach at create time. On create, `auth=UNSET` and `auth=None` both create a public upstream. Pass `TokenAuth` or `UsernamePasswordAuth` to store credentials.
## Response
Repository ID.
Organization slug.
Repository name.
Default bookmark name.
Current change ID at the default bookmark tip.
Configured upstream remote, or null when no upstream is configured.
Creation time.
Repository tags.
### UpstreamConfig
Upstream Git remote URL.
Stored upstream authentication kind, or null when the upstream is public.
# repos.delete()
Source: https://docs.mesa.dev/content/reference/py/repos-delete
Permanently delete a repository.
Delete a repository and its history. This operation is irreversible.
**Required scope: `write`**
```python theme={null}
result = await mesa.repos.delete(repo="app")
print(result.success)
```
## Options
Repository name to delete.
## Response
Whether the operation succeeded.
# repos.get()
Source: https://docs.mesa.dev/content/reference/py/repos-get
Fetch metadata for one repository.
Fetch a repository by name.
**Required scope: `read`**
```python theme={null}
repo = await mesa.repos.get(repo="app")
print(repo.id)
print(repo.default_bookmark)
```
## Options
Repository name.
## Response
Repository ID.
Organization slug.
Repository name.
Default bookmark name.
Current change ID at the default bookmark tip.
Configured upstream remote, or null when no upstream is configured.
Creation time.
Repository tags.
### UpstreamConfig
Upstream Git remote URL.
Stored upstream authentication kind, or null when the upstream is public.
## Errors
Raises `NotFoundError` when the repository does not exist.
# repos.get_upstream_sync()
Source: https://docs.mesa.dev/content/reference/py/repos-get-upstream-sync
Get one sync for a repository's configured upstream.
Fetch a sync by id through the repository's upstream.
**Required scope: `write`**
```python theme={null}
sync = await mesa.repos.get_upstream_sync(repo="app", sync_id="sync_...")
print(sync.id, sync.status)
```
## Options
Repository name.
Sync ID.
## Response
Returns a sync object.
Sync ID.
Repository ID.
Sync direction.
Current sync status.
Attempt number for this run.
`read`
# repos.list()
Source: https://docs.mesa.dev/content/reference/py/repos-list
List repositories in an organization.
List repositories using cursor pagination.
**Required scope: `read`**
```python theme={null}
repos = await mesa.repos.list(limit=50)
for repo in repos.repos:
print(repo.name, repo.default_bookmark, repo.head_change_id)
```
## Options
Opaque pagination cursor from a previous response.
Maximum number of repositories to return. The server maximum is `100`.
Filter results by repository tags. See [Tag Filters](#tag-filters) below.
### Tag Filters
Filter the repository list by the tags on each repository. Pass a dictionary where each key is a tag name:
```python theme={null}
await mesa.repos.list(
tags={
"kind": "memory", # kind equals "memory"
"environment": "prod", # environment equals "prod"
"region": ["us-east-1", "us-west-2"], # region is either value
}
)
```
A repository matches when every key matches: multiple keys in one dictionary are implicitly combined with the logical `$and` operator. The filter above is equivalent to:
```python theme={null}
await mesa.repos.list(
tags={
"$and": [{"kind": "memory"}, {"environment": "prod"}, {"region": ["us-east-1", "us-west-2"]}],
}
)
```
Combine `$and`, `$or`, and `$not` to build more complex queries:
```python theme={null}
await mesa.repos.list(
tags={
"$and": [
{"workload": {"$in": ["sync", "index"]}},
{"$or": [{"environment": "prod"}, {"environment": "staging"}]},
{"$not": {"lifecycle": "archived"}}, # excludes archived repos
]
}
)
```
Operator names start with `$` and are case-insensitive (`$and`, `$AND`, and `$AnD` are equivalent). The `$` prefix is reserved for operators, so a tag name can never start with `$` — but plain `and`, `or`, and `not` are ordinary tag names.
#### Value Operators
Tag values match exactly by default. Wrap the value in an operator dictionary for other comparisons:
| Operator | Example | Matches |
| -------------- | ------------------------------------------------- | ---------------------------------- |
| `$eq` | `{"environment": {"$eq": "prod"}}` | Exact tag value |
| `$in` | `{"region": {"$in": ["us-east-1", "us-west-2"]}}` | Any listed tag value |
| `$contains` | `{"owner": {"$contains": "agent"}}` | Tag value contains literal text |
| `$starts_with` | `{"region": {"$starts_with": "us-"}}` | Tag value starts with literal text |
| `$ends_with` | `{"owner": {"$ends_with": "@platform"}}` | Tag value ends with literal text |
| `$exists` | `{"archived": {"$exists": False}}` | Tag key presence or absence |
`$exists` checks only whether the tag key is present, regardless of its value. To match a specific value, use an exact match instead.
```python theme={null}
await mesa.repos.list(
tags={
"archived": {"$exists": False},
"lifecycle": {"$eq": "active"},
}
)
```
#### Complex Composition
This example selects active production-like skill or memory repos that are owned by platform teams and located in US regions:
```python theme={null}
result = await mesa.repos.list(
tags={
"$and": [
{"environment": {"$in": ["prod", "staging"]}},
{"region": {"$starts_with": "us-"}},
{
"$or": [
{"kind": "skill", "owner": {"$contains": "agent"}},
{"kind": "memory", "owner": {"$ends_with": "@platform"}},
]
},
{"$not": {"lifecycle": "archived"}},
{"archived": {"$exists": False}},
]
}
)
```
Matching semantics:
* Everything is case-insensitive: operator names, tag keys (normalized to lowercase), and tag value comparisons.
* `$and` matches when all child filters match, `$or` when any child matches, and `$not` negates its child filter.
* `$contains`, `$starts_with`, and `$ends_with` match literal text — `%`, `_`, and `\` in the search text are escaped, not treated as wildcards.
* Filters are limited to depth `8`, `64` total clauses, `32` items in `$and`, `$or`, and `$in` arrays, and `256` characters per tag key or value.
The Python SDK serializes filter dictionaries and sends them to the API. Invalid filters return `400 INVALID_REQUEST` from the server.
For direct REST calls, pass the filter as a JSON-encoded string in the `tags` query parameter:
```python theme={null}
import json
from urllib.parse import quote
tags = json.dumps({"$and": [{"environment": "prod"}, {"$not": {"lifecycle": "archived"}}]})
url = f"/v1/acme/repos?tags={quote(tags, safe='')}"
```
Map listed repositories into a layout to mount the filtered set:
```python theme={null}
from mesa_sdk import repo
result = await mesa.repos.list(tags={"$and": [{"environment": "prod"}, {"kind": "skill"}]})
async with mesa.fs(
layout={"/skills": [repo(r.name, mode="ro") for r in result.repos]},
authors=[{"name": "Workspace Agent", "email": "agent@example.com"}],
).mount() as fs:
...
```
## Response
Cursor for the next page, or null when no more results remain.
Whether another page of results is available.
Repository objects.
### Repo
Repository ID.
Organization slug.
Repository name.
Default bookmark name.
Current change ID at the default bookmark tip.
Configured upstream remote, or null when no upstream is configured.
Creation time.
Repository tags.
### UpstreamConfig
Upstream Git remote URL.
Stored upstream authentication kind, or null when the upstream is public.
# mesa.repos.list_upstream_syncs()
Source: https://docs.mesa.dev/content/reference/py/repos-list-upstream-syncs
List syncs for a repository's upstream.
List syncs for one repository's upstream, newest first.
```python theme={null}
result = await mesa.repos.list_upstream_syncs(repo="app", limit=20)
for sync in result.syncs:
print(sync.direction, sync.status, sync.created_at)
```
## Options
Repository name.
Opaque pagination cursor from a previous response.
Maximum number of syncs to return. The server maximum is `100`.
## Response
Cursor for the next page, or `None` when no more results remain.
Whether another page is available.
Sync objects with `id`, `repo_id`, `direction`, `status`, `attempt`, `stats`, `error`, `created_at`, `started_at`, and `finished_at`.
## Required scope
`read`
# repos.sync_upstream()
Source: https://docs.mesa.dev/content/reference/py/repos-sync-upstream
Enqueue a sync to a repository's configured upstream.
Enqueue a sync for a repository that has an upstream remote.
**Required scope: `write`**
```python theme={null}
sync = await mesa.repos.sync_upstream(repo="app", direction="pull")
print(sync.id, sync.status)
await mesa.repos.sync_upstream(
repo="app",
direction="pull",
ref_globs={
"branches": "main",
},
)
```
## Options
Repository name.
`pull` fetches from upstream into Mesa. `push` sends Mesa branches and tags to upstream.
Branch and tag glob filters for this sync. Omit this field to sync all supported branches and tags. If provided, include at least one of `branches` or `tags`.
Glob matched against upstream branch names, such as `main`, `release/*`, or `*`. Omit or use an empty string to match no branches. Do not include `refs/heads/`.
Glob matched against upstream tag names, such as `v1.*` or `*`. Omit or use an empty string to match no tags. Do not include `refs/tags/`.
## Response
Sync ID.
Repository ID.
Sync direction.
Current sync status.
Attempt number for this run.
Branch and tag glob filters for this run.
Per-ref sync stats, or null until the run reaches a terminal state.
Failure message, or null when no error is present.
Creation time.
Start time, or null if the run has not started.
Finish time, or null if the run has not finished.
### SyncRefGlobs
Branch-name glob for this sync.
Tag-name glob for this sync.
### SyncRunStats
Per-ref sync outcomes. This contains only source refs that matched `ref_globs`.
### SyncRunRef
Git ref name.
Previous OID, or null when the ref was newly created.
OID after the sync attempt.
Result for this ref.
# repos.update()
Source: https://docs.mesa.dev/content/reference/py/repos-update
Rename a repository, change its default bookmark, or update upstream configuration.
Update fields on a repository. Omitted keyword arguments are left unchanged.
**Required scope: `write`**
```python theme={null}
repo = await mesa.repos.update(
repo="app",
name="renamed-app",
default_bookmark="main",
)
```
Update upstream credentials without changing other repository fields:
```python theme={null}
from mesa_sdk.types import TokenAuth, UpstreamConfig
repo = await mesa.repos.update(
repo="app",
upstream=UpstreamConfig(
url="https://github.com/acme/app.git",
auth=TokenAuth(token="github_pat_..."),
),
)
```
## Options
Repository name to update.
New repository name.
New default bookmark name.
Omit to leave upstream unchanged. Pass `None` to remove the upstream entirely. Pass `UpstreamConfig` to set or replace the upstream URL.
## Upstream auth tri-state
`UpstreamConfig.auth` has different meanings on update:
| Value | Behavior |
| ------------------------------------- | --------------------------------------------------------------- |
| omitted / `UNSET` | Preserve the existing stored credential while updating the URL. |
| `None` | Clear the stored credential and make the upstream public. |
| `TokenAuth` or `UsernamePasswordAuth` | Replace the stored credential. |
## Response
Repository ID.
Organization slug.
Repository name.
Default bookmark name.
Current change ID at the default bookmark tip.
Configured upstream remote, or null when no upstream is configured.
Creation time.
Repository tags.
### UpstreamConfig
Upstream Git remote URL.
Stored upstream authentication kind, or null when the upstream is public.
# webhook_targets.clear_repo_filter()
Source: https://docs.mesa.dev/content/reference/py/webhook-targets-clear-repo-filter
Make a webhook target receive events for every repository.
Remove the per-repository filter from a webhook target.
**Required scope: `admin`**
```python theme={null}
target = await mesa.webhook_targets.clear_repo_filter(
webhook_target_id="wh_123",
)
print(target.repo_ids) # None
```
## Options
Webhook target ID.
## Response
Webhook target ID.
Webhook target name, or null when unnamed.
Delivery URL.
Event names delivered to this target.
Repository allowlist, or null when the target receives events for all repositories.
Creation time.
Last update time.
# webhook_targets.create()
Source: https://docs.mesa.dev/content/reference/py/webhook-targets-create
Register a webhook delivery target.
Create a webhook target. The response includes the signing secret once.
**Required scope: `admin`**
```python theme={null}
target = await mesa.webhook_targets.create(
url="https://example.com/mesa/webhook",
name="production-webhook",
events=["push", "change.created"],
repo_ids=["repo_123"],
)
print(target.id)
print(target.secret) # store immediately
```
## Options
HTTPS URL that receives webhook deliveries.
Human-readable target name.
Event types to subscribe to. Defaults to `['push']` when omitted.
Restrict deliveries to specific repository IDs. Omit to receive events for all repositories in the organization.
## Event names
Supported event names include `repo.created`, `repo.updated`, `repo.deleted`, `bookmark.created`, `bookmark.deleted`, `bookmark.moved`, `bookmark.merged`, `change.created`, `change.evolved`, `push`, `sync.queued`, `sync.in_progress`, `sync.completed`, and `sync.failed`.
## Response
Webhook target ID.
Webhook target name, or null when unnamed.
Delivery URL.
Event names delivered to this target.
Repository allowlist, or null when the target receives events for all repositories.
Creation time.
Last update time.
Webhook signing secret. Store it immediately; it is shown only once.
# webhook_targets.delete()
Source: https://docs.mesa.dev/content/reference/py/webhook-targets-delete
Delete a webhook delivery target.
Delete a webhook target.
**Required scope: `admin`**
```python theme={null}
result = await mesa.webhook_targets.delete(webhook_target_id="wh_123")
print(result.success)
```
## Options
Webhook target ID.
## Response
Whether the operation succeeded.
# webhook_targets.list()
Source: https://docs.mesa.dev/content/reference/py/webhook-targets-list
List webhook delivery targets for an organization.
List webhook targets using cursor pagination.
**Required scope: `admin`**
```python theme={null}
targets = await mesa.webhook_targets.list(limit=50)
for target in targets.webhook_targets:
print(target.id, target.url, target.events)
```
## Options
Opaque pagination cursor from a previous response.
Maximum number of webhook targets to return. The server maximum is `100`.
## Response
Cursor for the next page, or null when no more results remain.
Whether another page of results is available.
Webhook target objects.
### WebhookTarget
Webhook target ID.
Webhook target name, or null when unnamed.
Delivery URL.
Event names delivered to this target.
Repository allowlist, or null when the target receives events for all repositories.
Creation time.
Last update time.
# webhook_targets.update()
Source: https://docs.mesa.dev/content/reference/py/webhook-targets-update
Update a webhook delivery target.
Update fields on a webhook target. Omitted fields are left unchanged.
**Required scope: `admin`**
```python theme={null}
target = await mesa.webhook_targets.update(
webhook_target_id="wh_123",
url="https://example.com/mesa/new-webhook",
events=["push"],
)
```
## Options
Webhook target ID.
New delivery URL.
New target name.
Replacement event list.
Replacement repository ID filter. The high-level method treats `None` as omitted; use `clear_repo_filter()` to send an explicit `null`.
## Response
Webhook target ID.
Webhook target name, or null when unnamed.
Delivery URL.
Event names delivered to this target.
Repository allowlist, or null when the target receives events for all repositories.
Creation time.
Last update time.
# whoami()
Source: https://docs.mesa.dev/content/reference/py/whoami
Return the caller's authenticated organization and scopes.
`mesa.whoami()` calls the API once, caches the result, and also populates the client's default organization.
**Required scope: `read`**
```python theme={null}
identity = await mesa.whoami()
print(identity.org.slug)
```
## Options
This method does not take options.
## Response
Organization associated with the authenticated access token.
Effective scopes granted to the access token.
### OrgIdentity
Organization ID.
Organization slug.
Organization display name.
## Errors
Raises `AuthenticationError` for an invalid access token and other `ApiError` subclasses for non-2xx responses.
# bookmarks.create()
Source: https://docs.mesa.dev/content/reference/ts/bookmarks-create
Create a bookmark at a change.
Create a bookmark pointing at an existing change.
**Required scope: `write`**
```ts theme={null}
const bookmark = await mesa.bookmarks.create({
repo: 'app',
name: 'feature/auth',
change_id: 'zyxwvutsrqponmlkzyxwvutsrqponmlk',
});
console.log(bookmark.name);
```
## Options
Repository name.
Bookmark name.
Change ID the bookmark should point to.
## Response
Bookmark name.
Change ID the bookmark points to.
Whether this is the repository default bookmark.
# bookmarks.delete()
Source: https://docs.mesa.dev/content/reference/ts/bookmarks-delete
Delete a bookmark.
Delete a bookmark by name.
**Required scope: `write`**
```ts theme={null}
await mesa.bookmarks.delete({ repo: 'app', bookmark: 'feature/auth' });
```
## Options
Repository name.
Bookmark name.
## Response
Whether the operation succeeded.
# bookmarks.list()
Source: https://docs.mesa.dev/content/reference/ts/bookmarks-list
List bookmarks in a repository.
List branch-like bookmark refs using cursor pagination.
**Required scope: `read`**
```ts theme={null}
const bookmarks = await mesa.bookmarks.list({ repo: 'app', limit: 50 });
for (const bookmark of bookmarks.bookmarks) {
console.log(bookmark.name, bookmark.change_id);
}
```
## Options
Repository name.
Opaque pagination cursor from a previous response.
Maximum number of bookmarks to return. The server maximum is `100`.
## Response
Cursor for the next page, or null when no more results remain.
Whether another page of results is available.
Bookmark objects.
### Bookmark
Bookmark name.
Change ID the bookmark points to.
Whether this is the repository default bookmark.
# bookmarks.merge()
Source: https://docs.mesa.dev/content/reference/ts/bookmarks-merge
Merge one bookmark into another.
Merge `source` into `target` and move `target` to the merge result.
**Required scope: `write`**
```ts theme={null}
const result = await mesa.bookmarks.merge({
repo: 'app',
target: 'main',
source: 'feature/auth',
authors: [{ name: 'Docs Bot', email: 'docs@example.com' }],
delete_source: true,
});
console.log(result.merge_type, result.change_id);
```
Resolve conflicts by retrying with resolutions:
```ts theme={null}
await mesa.bookmarks.merge({
repo: 'app',
target: 'main',
source: 'feature/auth',
authors: [{ name: 'Docs Bot', email: 'docs@example.com' }],
resolutions: [{ path: 'README.md', take: 'source' }],
});
```
## Options
Repository name.
Bookmark being merged into. This bookmark is updated to the merge result.
Bookmark being merged from.
Delete the source bookmark after a successful merge. Defaults to `false`.
Persist a conflicted merge commit instead of rejecting with `MERGE_CONFLICT`. Defaults to `false`.
Conflict resolutions to apply before conflict detection. Each resolution targets a path and either supplies whole-file `content`, takes `target` or `source`, or resolves individual hunks.
Commit authors, in order, with at least one entry. Private-key clients set this.
## Response
Type of merge that was performed.
Whether the resulting merge change still contains unresolved conflicts.
Commit OID the target bookmark now points to.
Change ID the target bookmark points to after the merge.
Bookmark that was merged into.
Bookmark that was merged from.
Whether the source bookmark was deleted after the merge.
# bookmarks.move()
Source: https://docs.mesa.dev/content/reference/ts/bookmarks-move
Move a bookmark to another change.
Move an existing bookmark to a new change ID. Moves must advance history unless
`allow_backwards: true` is provided.
**Required scope: `write`**
```ts theme={null}
const bookmark = await mesa.bookmarks.move({
repo: 'app',
bookmark: 'main',
change_id: 'zyxwvutsrqponmlkzyxwvutsrqponmlk',
});
console.log(bookmark.from_change_id, bookmark.change_id);
```
## Options
Repository name.
Bookmark to move.
Target change ID.
Allow an intentional backward or sideways move.
## Response
Bookmark name.
Change ID the bookmark points to.
Whether this is the repository default bookmark.
Previous change ID before the move, or null when unavailable.
# changes.create()
Source: https://docs.mesa.dev/content/reference/ts/changes-create
Create a change on top of an existing base change.
Create a new change, optionally applying initial file operations atomically. File content must be base64-encoded.
**Required scope: `write`**
```ts theme={null}
import { Buffer } from 'node:buffer';
const change = await mesa.changes.create({
repo: 'app',
base_change_id: 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz',
message: 'Add README',
authors: [{ name: 'Docs Bot', email: 'docs@example.com' }],
files: [
{
path: 'README.md',
content: Buffer.from('# App\n').toString('base64'),
},
{ path: 'old.txt', action: 'delete' },
],
});
```
## Options
Repository name.
Change to create the new change on top of. Use Mesa's virtual root change ID for the first change in an empty repository.
Change description. Required when `files` is non-empty. When `files` is omitted, omit `message` to create the change with no description, or pass `""` explicitly.
Commit authors, in order, with at least one entry. Private-key clients set this.
Committer identity. When omitted, the first author is used.
File operations to apply atomically. Upserts use `{ path, content, encoding?, action?, mode? }`; deletes use `{ path, action: 'delete' }`.
## Response
Change ID.
Current commit OID for the change.
Whether the current commit still contains unresolved conflicts.
Current change description. Empty string means the change has no description.
Commit author identity.
Committer identity.
Parent change IDs for this change.
Creation time.
Last update time.
### CommitIdentity
Author or committer display name.
Author or committer email address.
Timestamp for this identity when provided.
# changes.get()
Source: https://docs.mesa.dev/content/reference/ts/changes-get
Get a change by ID.
Fetch a change and its path-level metadata.
**Required scope: `read`**
```ts theme={null}
const change = await mesa.changes.get({
repo: 'app',
change_id: 'zyxwvutsrqponmlkzyxwvutsrqponmlk',
});
console.log(change.files);
console.log(change.conflicts);
```
## Options
Repository name.
Change ID.
## Response
Change ID.
Current commit OID for the change.
Whether the current commit still contains unresolved conflicts.
Current change description. Empty string means the change has no description.
Complete ordered commit attribution. Every entry is an equal contributor.
Shared ISO 8601 authored timestamp. The virtual root uses the Unix epoch.
Committer identity. The virtual root has an empty name and email at the Unix epoch.
The deprecated REST `author` field is not returned by the SDK; read `authors` and `authored_at`.
Parent change IDs for this change.
Creation time.
Last update time.
For Mesa's virtual root, `authors` is `[]` and the signature fields are empty at the Unix epoch. Check `authors.length` to detect the root.
Repository-relative paths changed by this change.
Repository-relative paths that still contain unresolved conflicts.
### CommitIdentity
Author display name.
Author email address, or `null` when no email was supplied.
### CommitSignature
Committer display name.
Email address, or `""` when none was supplied. This string form is retained for compatibility.
Timestamp for this signature when provided.
# changes.list()
Source: https://docs.mesa.dev/content/reference/ts/changes-list
List changes reachable from a repository bookmark.
List changes in a repository using cursor pagination.
**Required scope: `write`**
```ts theme={null}
const changes = await mesa.changes.list({ repo: 'app', bookmark: 'main' });
for (const change of changes.changes) {
console.log(change.id, change.message);
}
```
## Options
Repository name.
Bookmark whose reachable changes should be listed. Defaults to the repository default bookmark.
Opaque pagination cursor from a previous response.
Maximum number of changes to return. The server maximum is `100`.
## Response
Cursor for the next page, or null when no more results remain.
Whether another page of results is available.
Change objects.
### Change
Change ID.
Current commit OID for the change.
Whether the current commit still contains unresolved conflicts.
Current change description. Empty string means the change has no description.
Complete ordered commit attribution. Every entry is an equal contributor.
Shared ISO 8601 authored timestamp. The virtual root uses the Unix epoch.
Committer identity. The virtual root has an empty name and email at the Unix epoch.
The deprecated REST `author` field is not returned by the SDK; read `authors` and `authored_at`.
Parent change IDs for this change.
Creation time.
Last update time.
### CommitIdentity
Author display name.
Author email address, or `null` when no email was supplied.
### CommitSignature
Committer display name.
Email address, or `""` when none was supplied. This string form is retained for compatibility.
Timestamp for this signature when provided.
# changes.patch()
Source: https://docs.mesa.dev/content/reference/ts/changes-patch
Update a change's metadata, files, or conflict resolutions.
Patch a change. You can update metadata, apply file operations, use a base commit guard, or apply conflict resolutions.
**Required scope: `write`**
```ts theme={null}
import { Buffer } from 'node:buffer';
const change = await mesa.changes.patch({
repo: 'app',
change_id: 'zyxwvutsrqponmlkzyxwvutsrqponmlk',
message: 'Update README',
authors: [{ name: 'Docs Bot', email: 'docs@example.com' }],
files: [
{
path: 'README.md',
content: Buffer.from('# Updated\n').toString('base64'),
},
],
});
```
Resolve conflicts:
```ts theme={null}
await mesa.changes.patch({
repo: 'app',
change_id: 'zyxwvutsrqponmlkzyxwvutsrqponmlk',
resolutions: [{ path: 'README.md', take: 'target' }],
});
```
A patch that applies conflict resolutions keeps the existing commit authors, so omit `authors`.
## Options
Repository name.
Change ID to patch.
Replacement change description. Omit to preserve the existing description; pass `""` to clear it.
Commit authors, in order, with at least one entry. Private-key clients set this, but omit it when you apply `resolutions`.
Replacement committer identity.
Non-empty file operations to apply. Cannot be combined with `resolutions`.
Optional optimistic concurrency guard. The patch fails if the change no longer points at this commit.
Conflict resolutions to apply, at least one. Cannot be combined with `files`.
## Response
Change ID.
Current commit OID for the change.
Whether the current commit still contains unresolved conflicts.
Current change description. Empty string means the change has no description.
Commit author identity.
Committer identity.
Parent change IDs for this change.
Creation time.
Last update time.
### CommitIdentity
Author or committer display name.
Author or committer email address.
Timestamp for this identity when provided.
# content.get()
Source: https://docs.mesa.dev/content/reference/ts/content-get
Read a file, symlink, or directory listing from a repository.
Read repository content without mounting MesaFS.
**Required scope: `read`**
```ts theme={null}
import { Buffer } from 'node:buffer';
const file = await mesa.content.get({ repo: 'app', path: 'README.md' });
if (file.type === 'file') {
console.log(Buffer.from(file.content, 'base64').toString('utf8'));
}
```
Read a directory listing:
```ts theme={null}
const root = await mesa.content.get({ repo: 'app', path: '', depth: 1 });
if (root.type === 'dir') {
for (const entry of root.entries) console.log(entry.type, entry.path);
}
```
## Options
Repository name.
Repository-relative path. Omit or pass an empty string for the repository root.
Change to read from. Defaults to the current change at the default bookmark tip.
Directory traversal depth from `0` to `10`.
## Response
Regular file content.
Symbolic link content.
Directory listing.
### ContentFile
Content variant discriminator.
Base name of the file.
Repository-relative path.
Git object SHA.
Whether this path is conflicted when known.
Size in bytes.
Encoding used for `content`.
Base64-encoded file bytes.
POSIX file mode.
Extended attributes for this path. Values are base64-encoded.
### ContentSymlink
Content variant discriminator.
Base name of the symlink.
Repository-relative path.
Git object SHA.
Whether this path is conflicted when known.
Size in bytes.
Encoding used for `content`.
Base64-encoded symlink target bytes.
POSIX symlink mode.
Extended attributes for this path. Values are base64-encoded.
### ContentDirectory
Content variant discriminator.
Base name of the directory.
Repository-relative path.
Git object SHA.
Number of direct children in this directory.
Directory entries returned for the requested depth.
Extended attributes attached to this directory. Values are base64-encoded.
### ContentDirectoryEntry
Entry variant discriminator.
Base name of the entry.
Repository-relative path.
Git object SHA.
Size in bytes for file and symlink entries.
POSIX mode for file and symlink entries.
# diffs.get()
Source: https://docs.mesa.dev/content/reference/ts/diffs-get
Compare two changes in a repository.
Get structured diff data between two changes.
**Required scope: `read`**
```ts theme={null}
const diff = await mesa.diffs.get({
repo: 'app',
base_change_id: 'zyxwvutsrqponmlkzyxwvutsrqponmlk',
head_change_id: 'yxwvutsrqponmlkzyxwvutsrqponmlkz',
conflicts: 'include',
});
console.log(diff.stats.additions, diff.stats.deletions);
```
Get only conflict details:
```ts theme={null}
const conflicts = await mesa.diffs.get({
repo: 'app',
base_change_id: 'zyxwvutsrqponmlkzyxwvutsrqponmlk',
head_change_id: 'yxwvutsrqponmlkzyxwvutsrqponmlkz',
conflicts: 'only',
});
```
## Options
Repository name.
Base change ID.
Head change ID.
Conflict detail mode. Defaults to `include`.
## Response
`entries[]` and `conflicted_entries[]` are mutually exclusive: unresolved conflicted paths appear only in `conflicted_entries[]`.
Base change used for comparison.
Head change used for comparison.
Aggregate counts for the response.
Whether the response hit the server entry limit and is incomplete.
Structural changed entries. Unchanged paths are omitted.
Conflict-only details for unresolved paths.
### DiffStats
Number of structural entries returned in `entries[]`.
Total added lines across textual diff hunks.
Total deleted lines across textual diff hunks.
Sum of `additions` and `deletions`.
Number of paths returned in `conflicted_entries[]`.
Total conflict hunk count across `conflicted_entries[]`.
### DiffEntry
Repository-relative path at the head change.
How the entry changed between the base and head changes.
Previous repository-relative path for renamed entries. Null for non-renames.
Approximate size in bytes of the changed entry content, not the textual diff.
Always false for entries returned in `entries[]`. Conflicted paths are returned in `conflicted_entries[]`.
Why textual hunk data is unavailable. Null when `hunks` is present.
Structured textual diff hunks. Null when `omitted_reason` is set.
Structured conflict hunks for the entry, or null when there are none. Structural entries in `entries[]` normally return null.
### DiffHunk
Starting line number in the base side.
Number of base-side lines covered by the hunk.
Starting line number in the head side.
Number of head-side lines covered by the hunk.
Lines in this textual diff hunk.
### DiffLine
Line classification inside a textual diff hunk.
Line text.
### ConflictedDiffEntry
Repository-relative path for the unresolved conflict.
Per-hunk conflict detail. Empty when `omitted_reason` is set or the conflict is non-textual.
Approximate size in bytes of the largest side of the conflict.
Why hunk data is unavailable. When set, fetch file bytes with `mesa.content.get()` against the target or source change to compose a whole-file resolution.
### DiffConflictHunk
Stable identifier for the conflict hunk.
Starting line number in the base side.
Number of base-side lines covered by the hunk.
Starting line number in the head side.
Number of head-side lines covered by the hunk.
Target side of the conflicted hunk.
Base side of the conflicted hunk.
Source side of the conflicted hunk.
### ChangeConflictHunkSide
Base64-encoded raw bytes for this side of the conflicted hunk.
# fs.bash()
Source: https://docs.mesa.dev/content/reference/ts/fs-bash
Run shell commands against a mounted Mesa filesystem.
`fs.bash(...)` builds a `just-bash` interpreter rooted at the mounted filesystem. Commands run against MesaFS; no host shell is spawned.
```ts theme={null}
const bash = fs.bash({ cwd: '/workspace', env: { CI: 'true' } });
const result = await bash.exec('rg -n TODO src | head -20');
console.log(result.exitCode);
console.log(result.stdout);
console.log(result.stderr);
```
## Options
Environment variables for the shell. The host process environment is not inherited unless you pass it explicitly.
Working directory inside the mount. Defaults to `/`.
Limits for command count, loops, output sizes, and related execution safety controls.
Restrict available built-in command names.
Register custom command implementations.
Enable network commands with explicit URL policy.
Custom secure fetch implementation used by network-enabled commands.
Python execution configuration. Disabled by default.
JavaScript execution configuration. Disabled by default.
Receives command execution logs.
## exec()
`exec(command)` takes one string containing one or more shell statements.
```ts theme={null}
const result = await bash.exec(`
rg -l integration .
node --test
`);
```
Shell script to execute.
## Response
`exec(...)` returns `ExecResult` from `just-bash`.
Standard output.
Standard error.
Process exit code. `0` indicates success.
Resulting shell environment when included by `just-bash`.
## Command failures
Command failures usually return a non-zero `exitCode`; they do not automatically raise.
```ts theme={null}
const result = await bash.exec('cat /workspace/missing.txt');
if (result.exitCode !== 0) {
console.error(result.stderr);
}
```
## Binary files
Use `fs.readFileBuffer(...)` for binary files. Text-oriented shell commands such as `cat` are intended for text output.
# fs.bookmark
Source: https://docs.mesa.dev/content/reference/ts/fs-bookmarks
Manage bookmarks inside a mounted Mesa filesystem.
Mounted filesystems expose bookmark controls under `fs.bookmark`.
## fs.bookmark.create()
Create a new bookmark on the current commit without switching to it.
```ts theme={null}
await fs.bookmark.create({ repo: 'app', name: 'feature/auth' });
```
Repository name.
Bookmark name.
## fs.bookmark.move()
Move an existing bookmark to point at a change.
Moves must advance history unless `allowBackwards: true` is provided.
```ts theme={null}
const current = await fs.change.current({ repo: 'app' });
await fs.bookmark.move({ repo: 'app', name: 'main', changeId: current.changeId });
```
Repository name.
Bookmark name.
Change ID to move the bookmark to.
Allow an intentional backward or sideways move.
## fs.bookmark.list()
List bookmark names for a repository.
```ts theme={null}
const bookmarks = await fs.bookmark.list({ repo: 'app' });
console.log(bookmarks);
```
Repository name.
## Response
`create(...)` and `move(...)` return `Promise`. `list(...)` returns `Promise`.
# fs.change
Source: https://docs.mesa.dev/content/reference/ts/fs-changes
Manage active changes inside a mounted Mesa filesystem.
Mounted filesystems expose change controls under `fs.change`.
## fs.change.new()
Create and switch to a new change from a bookmark or existing change.
```ts theme={null}
const result = await fs.change.new({ repo: 'app', bookmark: 'main', message: 'Try new copy' });
console.log(result.changeOid);
```
Repository name.
Bookmark to fork from. Mutually exclusive with `changeId`.
Change ID to fork from. Mutually exclusive with `bookmark`.
Description for the new change. Omit to create the change with no description; pass `""` explicitly for an empty description.
## fs.change.edit()
Switch to an existing editable change. This never creates a new change.
```ts theme={null}
await fs.change.edit({ repo: 'app', changeId: 'zyxwvutsrqponmlkzyxwvutsrqponmlk' });
```
Repository name.
Bookmark whose existing change should be edited. Mutually exclusive with `changeId`.
Existing change ID to edit. Mutually exclusive with `bookmark`.
## fs.change.list()
List changes reachable from the current checkout's commit.
```ts theme={null}
const changes = await fs.change.list({ repo: 'app', limit: 10 });
```
Repository name.
Maximum number of changes to return. Defaults to `50`. Pass `0` for no limit.
## fs.change.current()
Return the currently active change for a repo.
```ts theme={null}
const current = await fs.change.current({ repo: 'app' });
console.log(current.changeId, current.commitOid);
```
Repository name.
## Response types
Hex-encoded change OID of the now-active change.
Hex-encoded change ID.
Hex-encoded commit OID the change currently points to.
# mesa.fs()
Source: https://docs.mesa.dev/content/reference/ts/fs-mount
Define a layout, mount it as a Mesa virtual filesystem, or mint its token.
`mesa.fs({ layout, authors, ttl? })` builds a `FilesystemDefinition`: a raw `Layout`, its authors, and its token lifetime, bundled with `layout()`, `mount()`, and `token()`. Calling `.mount()` returns a `MesaFileSystem` backed by Mesa's native filesystem.
```ts theme={null}
import { Buffer } from 'node:buffer';
import { Mesa, repo } from '@mesadev/sdk';
const mesa = new Mesa({ privateKey: process.env.MESA_PRIVATE_KEY });
const fs = await mesa
.fs({
layout: {
'/workspace': repo('app', { mode: 'rw', at: { bookmark: 'main' } }),
},
authors: [{ name: 'Mesa Bot', email: 'mesa-bot@example.com' }],
})
.mount();
const data = await fs.readFileBuffer('/workspace/README.md');
console.log(Buffer.from(data).toString('utf8'));
```
Map `repos.list()` results into layout declarations when the repository set is dynamic:
```ts theme={null}
const { repos } = await mesa.repos.list({
tags: {
$and: [{ environment: 'prod' }, { workload: { $in: ['sync', 'index'] } }],
},
});
const fs = await mesa
.fs({
layout: {
'/skills': repos.map((r) => repo(r.name, { mode: 'ro' })),
},
authors: [{ name: 'Mesa Bot', email: 'mesa-bot@example.com' }],
})
.mount();
```
The client signs a short-lived, layout-scoped token locally from its private key. The mount keeps that token for its whole lifetime; nothing refreshes it in the background.
See [Layouts](/content/mesafs/layouts) for nesting, structural rules, and running a layout mount in a sandbox.
## `mesa.fs(options)`
Synchronous. Builds the definition eagerly so invalid layouts throw at this call.
Map of absolute mount paths (`/`-prefixed) to one `repo(...)` declaration or an array of them. The mount contains exactly this visible path tree.
Lifetime, in seconds, of every token the definition mints through `token()` and under the hood in `mount()`. Defaults to `900` and allows up to `14400`.
Commit authors, in order, with at least one entry. Required for every layout definition.
### Definition members
An independent plain-object snapshot of the validated layout. Pass it to `JSON.stringify(...)` to produce the document `mesa mount --layout` reads.
Mount the layout as the complete visible path tree. Accepts only runtime options (`cache`, `telemetry`). The mount's token lifetime is the definition's `ttl`.
Mint the layout-scoped, least-privilege access token. Each repository receives read only access for `mode: 'ro'` or read and write access for `mode: 'rw'`.
`mesa.fs(...)` validates the layout structurally before returning the definition. Repository names resolve only at mount time — a layout naming a nonexistent repository still produces a definition and token, then fails when the mount resolves it.
```ts theme={null}
import { repo } from '@mesadev/sdk';
const definition = mesa.fs({
layout: { '/workspace': repo('app', { mode: 'rw' }) },
authors: [{ name: 'Mesa Bot', email: 'mesa-bot@example.com' }],
ttl: 3600,
});
sandbox.writeFile('layout.json', JSON.stringify(definition.layout(), null, 2));
const { token } = await definition.token();
```
## `repo(selector, options)`
Declare one repository in a layout. Do not construct `Repo` objects by hand.
```ts theme={null}
repo('app', { mode: 'rw', at: { bookmark: 'main' } });
repo({ name: 'app' }, { mode: 'ro', at: { changeId: 'zyxwvutsrqponmlkzyxwvutsrqponmlk' } });
repo('app', {
mode: 'rw',
branchedFrom: {
bookmark: 'main',
as: { bookmark: 'run-27', describe: 'initiate plan...' },
},
});
```
Repository name within the client's organization.
Access mode. Always required — there is no default. `'ro'` rejects writes with `EROFS`.
Pin an existing revision: `{ bookmark }` or `{ changeId }`. Mutually exclusive with `branchedFrom`. When neither `at` nor `branchedFrom` is set, the repository's default bookmark is used.
Fork a new empty descendant from a parent tip at cold open and check it out. Requires `mode: 'rw'`. Shape: parent `{ bookmark }` or `{ changeId }`, optional `as: { bookmark?, describe? }`. Omit `as` (or `as.bookmark`) for an anonymous tip. Parent bookmark is not moved. Mutually exclusive with `at`.
Directory-name override. Only valid when the declaration is an element of an array value in the layout.
Nested repository mounts declared beneath this repository's mount path. Keys are relative paths.
## `FsMountRuntimeOptions`
Non-token options accepted by `definition.mount(options?)`.
Optional on-disk cache. When omitted, the mount uses in-memory caching only.
Minimum native log level. Defaults to `warn`.
Per-instance structured log callback from the native filesystem.
### Disk cache
```ts theme={null}
const fs = await mesa
.fs({
layout: { '/workspace': repo('app', { mode: 'rw' }) },
authors: [{ name: 'Mesa Bot' }],
})
.mount({
cache: { diskCache: { path: '/tmp/mesa-cache', maxSizeBytes: 500_000_000 } },
});
```
Directory for the on-disk cache.
Optional cache size cap. When omitted, the native extension auto-sizes the budget against system resources.
## Paths
Mounted paths are whatever the layout declares:
```text theme={null}
/workspace/README.md
/workspace/src/main.ts
/skills/code-review/SKILL.md
```
## Response
`definition.mount()` returns a `Promise`.
## MesaFileSystem
The returned `fs` object implements the `just-bash` filesystem interface and exposes async file I/O, metadata, traversal, mutation, Bash, and mounted-repo version-control helpers.
```ts theme={null}
await fs.mkdir('/workspace/src', { recursive: true });
await fs.writeFile('/workspace/src/main.ts', 'console.log("hello")\n');
const data = await fs.readFile('/workspace/src/main.ts', 'utf8');
```
### Byte and text I/O
Read a file as text using a Node-compatible encoding. The `binary` encoding follows the `just-bash` latin1 byte-string convention.
Read raw file bytes.
Replace file contents, creating the file if missing. Parent directories must already exist.
Append text or bytes to a file, creating it if missing.
Return whether a path exists. Follows symlinks.
### Metadata and traversal
Return metadata for a path, following symlinks. `mtime` is a JavaScript `Date`.
Return metadata for a path without following symlinks. `mtime` is a JavaScript `Date`.
Return entry names in a directory. Sort client-side if you need deterministic ordering.
Return entry names with file type flags.
Resolve symlinks and `..` segments to a canonical path.
Return the target of a symlink.
Join and normalize a path against a base path without touching the filesystem.
Return a synchronous snapshot of paths known to the filesystem.
### Mutations
Create a directory. With `{ recursive: true }`, create missing parents and do nothing when the path already exists as a directory.
Remove a file or directory. Use `{ recursive: true }` for non-empty directories and `{ force: true }` to ignore missing paths.
Copy a file or directory. Use `{ recursive: true }` for directories.
Move or rename a file or directory.
Set permission bits, such as `0o755`.
Create a symlink. Relative targets are stored verbatim and resolve against the parent of `linkPath` at read time.
Set access and modification times with JavaScript `Date` values.
Create a hard link if supported by the native filesystem implementation.
### Subscriptions
MesaFS reads and writes are realtime by default. Use subscriptions only when your process needs an event stream that identifies which paths changed, such as to refetch data and rerender a frontend.
Subscribe to filesystem invalidation events. The handler is called after the changed state is visible through this filesystem instance.
```ts theme={null}
const subscription = fs.subscribe(async (event) => {
if (!event.recursive) {
const content = await fs.readFile(event.path, 'utf8');
console.log(event.path, content);
}
});
subscription.unsubscribe();
```
Callback invoked for each filesystem invalidation.
Absolute MesaFS path that changed, such as `/workspace/src/index.ts`.
Whether descendants of `path` may have changed. Refresh any cached directory or subtree state below `path` when this is `true`.
Stop receiving events and close the underlying watcher.
### FsStat
`stat(...)` and `lstat(...)` return `FsStat`.
Whether the path is a regular file.
Whether the path is a directory.
Whether the path is a symlink. This is `false` from `stat(...)` when the target exists because `stat` follows symlinks.
POSIX mode bits.
Size in bytes.
Modification time.
### Related filesystem methods
| Method | Reference |
| ------------------- | ------------------------------------------------------ |
| `fs.bash(...)` | [fs.bash()](/content/reference/ts/fs-bash) |
| `fs.change` | [fs.change](/content/reference/ts/fs-changes) |
| `fs.bookmark` | [fs.bookmark](/content/reference/ts/fs-bookmarks) |
| `fs.subscribe(...)` | [Advanced Realtime](/content/mesafs/advanced/realtime) |
## Errors
Throws `InvalidOptionsError` for a missing or empty layout, a `ttl` outside the private-key range (`1..14400` seconds), missing or rejected `authors`, or a layout that breaks the [structural rules](/content/mesafs/layouts#declaration-fields). A missing layout and author problems fail at the `mesa.fs(...)` call; the empty-layout and structural checks run at `mount()` or `token()`, before any token is minted. Token signing or VCS connection failures can throw API errors or connection errors.
# Overview
Source: https://docs.mesa.dev/content/reference/ts/index
The Mesa TypeScript SDK is the ergonomic client for Mesa in Node.js and JavaScript runtimes. It wraps the generated `@mesadev/rest` client, reads the organization from the private key, verifies webhooks, and exposes a native virtual filesystem for repo I/O and shell execution.
Node.js 18 or newer is required.
## Installation
```bash theme={null}
npm install @mesadev/sdk
```
## Create a client
```ts theme={null}
import { Mesa } from '@mesadev/sdk';
const mesa = new Mesa({ privateKey: process.env.MESA_PRIVATE_KEY });
const repos = await mesa.repos.list({ limit: 50 });
console.log(`found ${repos.repos.length} repos`);
```
Set `MESA_PRIVATE_KEY` in a trusted Node.js environment to call `new Mesa()` without an argument.
## Client options
```ts theme={null}
import { Mesa } from '@mesadev/sdk';
const mesa = new Mesa({ privateKey: 'mesa_private_key_acme_...', apiUrl: 'https://api.mesa.dev/v1',
fetch: globalThis.fetch,
userAgent: 'my-app/1.0.0',
webhookSecret: 'whsec_...' });
```
Ed25519 private key. Omit it to read `MESA_PRIVATE_KEY` in Node.js.
REST API base URL. Defaults to `https://api.mesa.dev/v1`. `http` and `https` are accepted. Trailing slashes are stripped.
Custom fetch implementation for REST requests.
Appended to the SDK user agent. Node.js uses `User-Agent`; browser-like runtimes use `X-Mesa-User-Agent`.
Signing secret used by `mesa.webhooks.receive(...)`.
## Lifecycle
The TypeScript client does not hold an HTTP session and does not need to be closed. Reuse one `Mesa` instance where practical.
Mounts sign one short-lived, layout-scoped access token locally from the client's private key and use it for the mount's whole lifetime. Definitions default to a 15 minute `ttl` and can run up to 4 hours. Set `ttl` on the definition: `mesa.fs({ layout, ttl, authors }).mount()`.
## Organization resolution
The client reads its organization from the private key. Resource methods always use that organization and do not accept an `org` value.
```ts theme={null}
console.log(mesa.org.slug);
await mesa.repos.list();
```
Use `mesa.org.get()` when you need organization metadata from the API.
## Resource APIs
| API | Purpose |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mesa.repos` | Create, read, update, delete, sync upstreams, and read upstream sync history on repositories. |
| `mesa.content` | Read files, symlinks, and directory listings without mounting. |
| `mesa.changes` | Create, patch, and inspect Mesa changes. |
| `mesa.diffs` | Inspect diffs and conflicts between changes. |
| `mesa.bookmarks` | Manage branch-like bookmark refs. |
| `mesa.webhookTargets` | Manage outbound webhook targets. |
| `mesa.webhooks` | Verify incoming webhook requests and dispatch typed handlers. |
| `mesa.fs` | Define a layout and mount it as a virtual filesystem, or mint its scoped token. Call `mesa.fs({ layout, authors, ttl? })` then `.mount()` / `.token()`. |
| `mesa.org` | Read the private key's organization slug with `mesa.org.slug`, or fetch organization metadata with `mesa.org.get()`. |
Bulk repository tag updates are available through `mesa.raw`.
## Response objects
High-level SDK methods return response data directly. They do not return generated HTTP wrapper objects.
```ts theme={null}
const repos = await mesa.repos.list();
for (const repo of repos.repos) {
console.log(repo.name, repo.head_change_id);
}
```
Generated response aliases such as `GetRepoResponse` live in `@mesadev/rest`.
## Common types
Import SDK-owned types from `@mesadev/sdk`.
```ts theme={null}
import type {
FilesystemDefinition,
FsMountRuntimeOptions,
Layout,
MesaOptions,
WebhookEvent,
} from '@mesadev/sdk';
import { repo } from '@mesadev/sdk';
```
| Type | Purpose |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MesaOptions` | Options accepted by the `Mesa` constructor. |
| `FsMountRuntimeOptions` | Runtime options for `definition.mount(...)` (`cache`, `telemetry`). |
| `FilesystemDefinition` | Layout, authors, and token lifetime bundled with the `layout()`, `mount()`, and `token()` operations they configure, returned by `mesa.fs({ layout, authors })`. |
| `AccessToken` | Short-lived credential returned by `definition.token()`, scoped to the layout that minted it, with `token` and ISO `expires_at` fields. |
| `Layout` | Raw path map validated by `mesa.fs(...)` and returned by `definition.layout()`. Serialize it for the CLI with `JSON.stringify(...)`. |
| `Repo` / `RepoOptions` | One layout repository declaration, produced by `repo(...)`. |
| `MesaFileSystem` | Native filesystem implementation returned by `mesa.fs({ layout, authors }).mount()`. |
| `ChangeInfo` | Change metadata returned by mounted filesystem change operations. |
| `ExecResult` | Output from `fs.bash().exec(...)`, re-exported from `just-bash`. |
| `WebhookEvent` | Discriminated union of webhook event payloads. |
High-level REST method input types are inferred from the method signatures. If you need named REST schema types, import them from `@mesadev/rest`.
## Error model
SDK-owned setup and webhook errors extend `MesaError` and expose a stable `code` field.
| Exception | Code | Meaning |
| ------------------------------ | ----------------------------- | ---------------------------------------------------------------------------------- |
| `MissingPrivateKeyError` | `MISSING_PRIVATE_KEY` | No private key was provided or available in the environment. |
| `MissingAccessTokenError` | `MISSING_ACCESS_TOKEN` | `MesaFileSystem.createAsync()` received an empty access token. |
| `InvalidApiUrlError` | `INVALID_API_URL` | `apiUrl` is malformed or uses a protocol other than `http` or `https`. |
| `InvalidOptionsError` | `INVALID_OPTIONS` | Local options are invalid, such as an empty layout or missing private-key authors. |
| `OrgResolutionError` | `ORG_RESOLUTION_FAILED` | An explicit `mesa.whoami()` request failed. |
| `MissingWebhookSecretError` | `MISSING_WEBHOOK_SECRET` | `mesa.webhooks.receive(...)` was called without a constructor `webhookSecret`. |
| `MesaWebhookVerificationError` | `WEBHOOK_VERIFICATION_FAILED` | Webhook signature, timestamp, JSON parsing, or payload validation failed. |
API operations throw Mesa API error payloads directly when the server returns an error response. These payloads are not `MesaError` instances.
## Raw generated client
`mesa.raw` exposes generated REST operations with authentication, base URL, fetch, and user-agent already wired in. Use it when the high-level SDK does not expose an operation or option yet.
```ts theme={null}
const repo = await mesa.raw.getRepo({
path: { org: 'acme', repo: 'app' },
});
```
Raw calls use the generated REST request shape (`path`, `query`, `body`) and return response data directly.
## Complete example
```ts theme={null}
import { Buffer } from 'node:buffer';
import { Mesa, repo } from '@mesadev/sdk';
const mesa = new Mesa({ privateKey: process.env.MESA_PRIVATE_KEY });
const created = await mesa.repos.create({ name: 'demo' });
const change = await mesa.changes.create({
repo: created.name,
base_change_id: created.head_change_id,
message: 'Add README',
authors: [{ name: 'Docs Bot', email: 'docs@example.com' }],
files: [
{
path: 'README.md',
content: Buffer.from('# Demo\n').toString('base64'),
},
],
});
await mesa.bookmarks.move({
repo: created.name,
bookmark: created.default_bookmark,
change_id: change.id,
});
const fs = await mesa
.fs({
layout: { '/workspace': repo(created.name, { mode: 'rw' }) },
authors: [{ name: 'Docs Bot', email: 'docs@example.com' }],
})
.mount();
const data = await fs.readFileBuffer('/workspace/README.md');
console.log(Buffer.from(data).toString('utf8'));
```
# org.get()
Source: https://docs.mesa.dev/content/reference/ts/org-get
Get metadata for an organization.
Get the organization encoded in the client's private key.
**Required scope: `read`**
```ts theme={null}
const org = await mesa.org.get();
console.log(org.slug, org.name);
```
## Response
Organization creation time.
Number of repositories in the organization.
# repos.create()
Source: https://docs.mesa.dev/content/reference/ts/repos-create
Create a Mesa repository, optionally with a default bookmark or upstream remote.
Create a repository in the organization encoded in the client's private key.
**Required scope: `write`**
```ts theme={null}
const repo = await mesa.repos.create({ name: 'app' });
console.log(repo.name);
console.log(repo.head_change_id);
```
Create with an upstream Git remote:
```ts theme={null}
const repo = await mesa.repos.create({
name: 'app',
default_bookmark: 'main',
upstream: {
url: 'https://github.com/acme/app.git',
auth: { kind: 'token', token: 'github_pat_...', token_username: 'bot' },
},
});
```
## Options
Repository name. If omitted, the API generates a name.
Default bookmark name. Defaults to `main` when omitted.
String key-value repository metadata. Keys cannot start with `$` (reserved for tag filter operators).
Optional upstream Git remote to attach at create time. On create, omitting `auth` and passing `auth: null` both create a public upstream. Pass token or username/password auth to store credentials.
## Response
Repository ID.
Organization slug.
Repository name.
Default bookmark name.
Current change ID at the default bookmark tip.
Configured upstream remote, or null when no upstream is configured.
Creation time.
Repository tags.
### UpstreamConfig
Upstream Git remote URL.
Stored upstream authentication kind, or null when the upstream is public.
# repos.delete()
Source: https://docs.mesa.dev/content/reference/ts/repos-delete
Delete a repository and its data.
Delete a repository by name.
**Required scope: `write`**
```ts theme={null}
await mesa.repos.delete({ repo: 'old-app' });
```
## Options
Repository name.
## Response
Whether the operation succeeded.
# repos.get()
Source: https://docs.mesa.dev/content/reference/ts/repos-get
Get a repository by name.
Fetch one repository in the organization encoded in the client's private key.
**Required scope: `read`**
```ts theme={null}
const repo = await mesa.repos.get({ repo: 'app' });
console.log(repo.id, repo.head_change_id);
```
## Options
Repository name.
## Response
Repository ID.
Organization slug.
Repository name.
Default bookmark name.
Current change ID at the default bookmark tip.
Configured upstream remote, or null when no upstream is configured.
Creation time.
Repository tags.
### UpstreamConfig
Upstream Git remote URL.
Stored upstream authentication kind, or null when the upstream is public.
# mesa.repos.getUpstreamSync()
Source: https://docs.mesa.dev/content/reference/ts/repos-get-upstream-sync
Get one sync for a repository upstream remote.
Fetch a sync by id through the repository's upstream.
```ts theme={null}
const run = await mesa.repos.getUpstreamSync({ repo: 'app', syncId: 'sync_...' });
console.log(run.id, run.status);
```
## Options
Repository name.
Sync ID.
## Response
Returns a sync object with fields such as `id`, `repo_id`, `direction`, `status`, `attempt`, `stats`, `error`, `created_at`, `started_at`, and `finished_at`.
## Required scope
`read`
# repos.list()
Source: https://docs.mesa.dev/content/reference/ts/repos-list
List repositories in an organization.
List repositories using cursor pagination.
**Required scope: `read`**
```ts theme={null}
const repos = await mesa.repos.list({ limit: 50 });
for (const repo of repos.repos) {
console.log(repo.name, repo.default_bookmark, repo.head_change_id);
}
```
## Options
Opaque pagination cursor from a previous response.
Maximum number of repositories to return. The server maximum is `100`.
Filter results by repository tags. See [Tag Filters](#tag-filters) below.
### Tag Filters
Filter the repository list by the tags on each repository. Pass an object where each key is a tag name:
```ts theme={null}
await mesa.repos.list({
tags: {
kind: 'memory', // kind equals "memory"
environment: 'prod', // environment equals "prod"
region: ['us-east-1', 'us-west-2'], // region is either value
},
});
```
A repository matches when every key matches: multiple keys in one object are implicitly combined with the logical `$and` operator. The filter above is equivalent to:
```ts theme={null}
await mesa.repos.list({
tags: {
$and: [{ kind: 'memory' }, { environment: 'prod' }, { region: ['us-east-1', 'us-west-2'] }],
},
});
```
Combine `$and`, `$or`, and `$not` to build more complex queries:
```ts theme={null}
await mesa.repos.list({
tags: {
$and: [
{ workload: { $in: ['sync', 'index'] } },
{ $or: [{ environment: 'prod' }, { environment: 'staging' }] },
{ $not: { lifecycle: 'archived' } }, // excludes archived repos
],
},
});
```
Operator names start with `$` and are case-insensitive (`$and`, `$AND`, and `$AnD` are equivalent). The `$` prefix is reserved for operators, so a tag name can never start with `$` — but plain `and`, `or`, and `not` are ordinary tag names.
#### Value Operators
Tag values match exactly by default. Wrap the value in an operator object for other comparisons:
| Operator | Example | Matches |
| -------------- | ------------------------------------------------- | ---------------------------------- |
| `$eq` | `{ environment: { $eq: 'prod' } }` | Exact tag value |
| `$in` | `{ region: { $in: ['us-east-1', 'us-west-2'] } }` | Any listed tag value |
| `$contains` | `{ owner: { $contains: 'agent' } }` | Tag value contains literal text |
| `$starts_with` | `{ region: { $starts_with: 'us-' } }` | Tag value starts with literal text |
| `$ends_with` | `{ owner: { $ends_with: '@platform' } }` | Tag value ends with literal text |
| `$exists` | `{ archived: { $exists: false } }` | Tag key presence or absence |
`$exists` checks only whether the tag key is present, regardless of its value. To match a specific value, use an exact match instead.
```ts theme={null}
await mesa.repos.list({
tags: {
archived: { $exists: false },
lifecycle: { $eq: 'active' },
},
});
```
#### Complex Composition
This example selects active production-like skill or memory repos that are owned by platform teams and located in US regions:
```ts theme={null}
const result = await mesa.repos.list({
tags: {
$and: [
{ environment: { $in: ['prod', 'staging'] } },
{ region: { $starts_with: 'us-' } },
{
$or: [
{ kind: 'skill', owner: { $contains: 'agent' } },
{ kind: 'memory', owner: { $ends_with: '@platform' } },
],
},
{ $not: { lifecycle: 'archived' } },
{ archived: { $exists: false } },
],
},
});
```
Matching semantics:
* Everything is case-insensitive: operator names, tag keys (normalized to lowercase), and tag value comparisons.
* `$and` matches when all child filters match, `$or` when any child matches, and `$not` negates its child filter.
* `$contains`, `$starts_with`, and `$ends_with` match literal text — `%`, `_`, and `\` in the search text are escaped, not treated as wildcards.
* Filters are limited to depth `8`, `64` total clauses, `32` items in `$and`, `$or`, and `$in` arrays, and `256` characters per tag key or value.
The TypeScript SDK serializes filter objects and sends them to the API. Invalid filters return `400 INVALID_REQUEST` from the server.
For direct REST calls, pass the filter as a JSON-encoded string in the `tags` query parameter:
```ts theme={null}
const tags = JSON.stringify({
$and: [{ environment: 'prod' }, { $not: { lifecycle: 'archived' } }],
});
await fetch(`/v1/acme/repos?tags=${encodeURIComponent(tags)}`);
```
Map listed repositories into a layout to mount the filtered set:
```ts theme={null}
import { repo } from '@mesadev/sdk';
const { repos } = await mesa.repos.list({
tags: { $and: [{ environment: 'prod' }, { kind: 'skill' }] },
});
const fs = await mesa
.fs({
layout: {
'/skills': repos.map((r) => repo(r.name, { mode: 'ro' })),
},
authors: [{ name: 'Workspace Agent', email: 'agent@example.com' }],
})
.mount();
```
## Response
Cursor for the next page, or null when no more results remain.
Whether another page of results is available.
Repository objects.
### Repo
Repository ID.
Organization slug.
Repository name.
Default bookmark name.
Current change ID at the default bookmark tip.
Configured upstream remote, or null when no upstream is configured.
Creation time.
Repository tags.
### UpstreamConfig
Upstream Git remote URL.
Stored upstream authentication kind, or null when the upstream is public.
# mesa.repos.listUpstreamSyncs()
Source: https://docs.mesa.dev/content/reference/ts/repos-list-upstream-syncs
List syncs for a repository's upstream.
List syncs for one repository's upstream, newest first.
```ts theme={null}
const { syncs, next_cursor, has_more } = await mesa.repos.listUpstreamSyncs({
repo: 'app',
limit: 20,
});
for (const sync of syncs) {
console.log(sync.direction, sync.status, sync.created_at);
}
```
## Options
Repository name.
Opaque pagination cursor from a previous response.
Maximum number of syncs to return. The server maximum is `100`.
## Response
Cursor for the next page, or `null` when no more results remain.
Whether another page is available.
Sync objects with `id`, `repo_id`, `direction`, `status`, `attempt`, `stats`, `error`, `created_at`, `started_at`, and `finished_at`.
## Required scope
`read`
# mesa.repos.syncUpstream()
Source: https://docs.mesa.dev/content/reference/ts/repos-sync-upstream
Sync a repository upstream remote.
Enqueue a sync for the configured upstream remote.
```ts theme={null}
const run = await mesa.repos.syncUpstream({ repo: 'app', direction: 'pull' });
console.log(run.id, run.status);
await mesa.repos.syncUpstream({
repo: 'app',
direction: 'pull',
ref_globs: {
branches: 'main',
},
});
```
## Options
Repository name.
`pull` fetches from upstream into Mesa. `push` sends Mesa branches and tags to upstream.
Branch and tag glob filters for this sync. Omit this field to sync all supported branches and tags. If provided, include at least one of `branches` or `tags`.
Glob matched against upstream branch names, such as `main`, `release/*`, or `*`. Omit or use an empty string to match no branches. Do not include `refs/heads/`.
Glob matched against upstream tag names, such as `v1.*` or `*`. Omit or use an empty string to match no tags. Do not include `refs/tags/`.
## Response
Returns a sync object with fields such as `id`, `repo_id`, `direction`, `status`, `attempt`, `ref_globs`, `stats`, `error`, `created_at`, `started_at`, and `finished_at`.
`ref_globs` is returned as normalized branch and tag glob filters. `stats.refs` contains only source refs that matched the filters.
## Required scope
`write`
# repos.update()
Source: https://docs.mesa.dev/content/reference/ts/repos-update
Update repository metadata, tags, default bookmark, or upstream remote.
Update mutable repository fields.
**Required scope: `write`**
```ts theme={null}
const repo = await mesa.repos.update({
repo: 'app',
default_bookmark: 'main',
tags: { env: 'prod', stale: null },
});
```
## Options
Current repository name.
New repository name.
New default bookmark name.
Patch repository tags. String values set tags, `null` removes tags, and omitted keys are unchanged. Keys cannot start with `$` (reserved for tag filter operators).
Set, replace, or remove the upstream remote. Omit to preserve the current upstream, pass `null` to remove it, or pass an object to attach/update it.
## Upstream auth updates
On update, upstream credentials are tri-state: omit `upstream` to preserve the whole upstream, pass `upstream: null` to remove it, pass `upstream.auth: null` to clear credentials, or pass token / username-password auth to replace credentials.
## Response
Repository ID.
Organization slug.
Repository name.
Default bookmark name.
Current change ID at the default bookmark tip.
Configured upstream remote, or null when no upstream is configured.
Creation time.
Repository tags.
### UpstreamConfig
Upstream Git remote URL.
Stored upstream authentication kind, or null when the upstream is public.
# webhookTargets.create()
Source: https://docs.mesa.dev/content/reference/ts/webhook-targets-create
Register a webhook delivery target.
Create a webhook target. The response includes the signing secret once.
**Required scope: `admin`**
```ts theme={null}
const target = await mesa.webhookTargets.create({
url: 'https://example.com/mesa/webhook',
name: 'production-webhook',
events: ['push', 'change.created'],
repo_ids: ['repo_123'],
});
console.log(target.id);
console.log(target.secret); // store immediately
```
## Options
HTTPS URL that receives webhook deliveries.
Human-readable target name.
Event types to subscribe to. Defaults to `['push']` when omitted.
Restrict deliveries to specific repository IDs. Omit to receive events for all repositories in the organization.
## Event names
Supported event names include `repo.created`, `repo.updated`, `repo.deleted`, `bookmark.created`, `bookmark.deleted`, `bookmark.moved`, `bookmark.merged`, `change.created`, `change.evolved`, `push`, `sync.queued`, `sync.in_progress`, `sync.completed`, and `sync.failed`.
## Response
Webhook target ID.
Webhook target name, or null when unnamed.
Delivery URL.
Event names delivered to this target.
Repository allowlist, or null when the target receives events for all repositories.
Creation time.
Last update time.
Webhook signing secret. Store it immediately; it is shown only once.
# webhookTargets.delete()
Source: https://docs.mesa.dev/content/reference/ts/webhook-targets-delete
Delete a webhook delivery target.
Delete a webhook target by ID.
**Required scope: `admin`**
```ts theme={null}
await mesa.webhookTargets.delete({ webhookTargetId: 'wht_123' });
```
## Options
Webhook target ID.
## Response
Whether the operation succeeded.
# webhookTargets.list()
Source: https://docs.mesa.dev/content/reference/ts/webhook-targets-list
List webhook delivery targets.
List configured webhook targets using cursor pagination.
**Required scope: `admin`**
```ts theme={null}
const targets = await mesa.webhookTargets.list({ limit: 50 });
for (const target of targets.webhook_targets) {
console.log(target.id, target.url, target.events);
}
```
## Options
Opaque pagination cursor from a previous response.
Maximum number of targets to return. The server maximum is `100`.
## Response
Cursor for the next page, or null when no more results remain.
Whether another page of results is available.
Webhook target objects.
### WebhookTarget
Webhook target ID.
Webhook target name, or null when unnamed.
Delivery URL.
Event names delivered to this target.
Repository allowlist, or null when the target receives events for all repositories.
Creation time.
Last update time.
# webhookTargets.update()
Source: https://docs.mesa.dev/content/reference/ts/webhook-targets-update
Update a webhook delivery target.
Update a webhook target. Omitted fields are left unchanged.
**Required scope: `admin`**
```ts theme={null}
const target = await mesa.webhookTargets.update({
webhookTargetId: 'wht_123',
events: ['push', 'change.created'],
});
```
Clear the repository filter and make the target organization-wide:
```ts theme={null}
await mesa.webhookTargets.update({
webhookTargetId: 'wht_123',
repo_ids: null,
});
```
## Options
Webhook target ID.
New delivery URL.
New human-readable name. Pass `null` to clear it.
Replacement event subscription list.
Replacement repository filter. Pass `null` to clear the filter and receive organization-wide events.
## Response
Webhook target ID.
Webhook target name, or null when unnamed.
Delivery URL.
Event names delivered to this target.
Repository allowlist, or null when the target receives events for all repositories.
Creation time.
Last update time.
# webhooks.on()
Source: https://docs.mesa.dev/content/reference/ts/webhooks-on
Register typed local webhook handlers.
Register handlers that run after `mesa.webhooks.receive(...)` verifies and validates an incoming webhook request.
```ts theme={null}
mesa.webhooks.on('push', async (event) => {
for (const update of event.data.updates) {
console.log(update.bookmark, update.action);
}
});
```
Register the same handler for multiple event names:
```ts theme={null}
mesa.webhooks.on(['push', 'repo.created'], async (event) => {
console.log(event.type, event.id);
});
```
## Options
Event name or event names to handle.
Function called with the typed event. It may return `void` or a promise.
## Event names
Supported event names include `repo.created`, `repo.updated`, `repo.deleted`, `bookmark.created`, `bookmark.deleted`, `bookmark.moved`, `bookmark.merged`, `change.created`, `change.evolved`, `push`, `sync.queued`, `sync.in_progress`, `sync.completed`, and `sync.failed`.
## Response
Returns `void`.
## Handler errors
Handlers are run during `mesa.webhooks.receive(...)`. If one or more handlers reject, `receive` throws an `AggregateError` after all matching handlers have settled.
# webhooks.receive()
Source: https://docs.mesa.dev/content/reference/ts/webhooks-receive
Verify an incoming webhook request and dispatch registered handlers.
Verify the Mesa webhook signature, validate the payload with the exported Zod schemas, and dispatch handlers registered with `mesa.webhooks.on(...)`.
```ts theme={null}
import { Mesa } from '@mesadev/sdk';
const mesa = new Mesa({ privateKey: process.env.MESA_PRIVATE_KEY, webhookSecret: process.env.MESA_WEBHOOK_SECRET });
mesa.webhooks.on('push', async (event) => {
console.log(event.repository?.name, event.data.updates.length);
});
export async function POST(request: Request) {
await mesa.webhooks.receive(request);
return new Response('ok');
}
```
## Options
Incoming request object containing the raw JSON body and `x-mesa-signature` header.
## Constructor requirement
`receive(...)` requires `webhookSecret` in the `Mesa` constructor's second argument. If omitted, it throws `MissingWebhookSecretError`.
## Signature format
The `x-mesa-signature` header contains query-style parts separated by commas, including `t` for timestamp and `sha256` for the HMAC SHA-256 digest.
## Response
Returns `Promise` when verification, validation, and handler dispatch succeed.
## Errors
Thrown when `webhookSecret` was not configured.
Thrown when the signature header is missing or malformed, the timestamp is outside the five-minute tolerance, the body is not JSON, or the payload fails schema validation.
Thrown when one or more registered handlers reject after the webhook itself has been verified.
## Schemas
The SDK exports `WebhookEventSchema` plus individual payload schemas such as `PushPayloadSchema`, `RepoCreatedPayloadSchema`, and `ChangeCreatedPayloadSchema` for callers that need standalone validation.
# whoami()
Source: https://docs.mesa.dev/content/reference/ts/whoami
Return the caller's authenticated organization and scopes.
Fetch the authenticated scopes and default organization.
**Required scope: `read`**
```ts theme={null}
const identity = await mesa.whoami();
console.log(identity.org.slug);
console.log(identity.scopes);
```
## Options
This method does not accept options.
## Response
Organization associated with the authenticated access token.
Effective scopes granted to the access token.
### OrgIdentity
Organization ID.
Organization slug.
Organization display name.
# Overview
Source: https://docs.mesa.dev/content/usage-patterns/overview
Recommended patterns for using Mesa in agent products.
Mesa works best when you have a clear notion of:
* Which data lives in each repository
* When to branch and checkpoint work
* When you will promote to the main line of version history
# Repositories
A repository is just a folder with independent version history and permissions.
Generally, you documents should be grouped into the same repository if they need to evolve together and share
the same owner. For example, all of the code for an application would typically live in a single repository. But
apps belonging to different tenants should live in separate repositories.
You can always add metadata to repositories to help you track which repositories belong to different teams or tenants.
Here are recommendations for common cases:
* Applications: One repo per application
* Knowledge bases: One repo per team or project
* n8n-style workflows: One repo per workflow
* Skills: One repo per skill. It is also feasible to create one skills repo per tenant,
but repo-per-skill plus metadata tags typically provides the most flexibility.
# Branching and checkpointing
Generally, you should treat `main` bookmark as the canonical state that users or production systems trust.
Treat other bookmarks as markers of the tips of different "timelines" -- work-in-progress modifications that are not yet ready to
be merged into the canonical state of the repository.
When an agent or human wants to draft some work, you typically create a new change forked off of the `main` bookmark, and optionally
create a new bookmark pointing at that change.
When you want to checkpoint your draft, you can chain a new change on top and advance the bookmark.
When you're happy with that draft work, you can merge it into the canonical state with the `mesa.merge` method.
Below is guidance on using Mesa for agent sessions.
### Timeline-per-session
Create one bookmark per user session/workflow run instead of mixing multiple sessions on `main`. Each session can evolve independently, and you can merge or discard without affecting parallel work.
```typescript TypeScript theme={null}
await mesa.bookmarks.create({
repo: "my-repo",
name: `session/${sessionId}`,
change_id: repo.head_change_id,
});
```
```python Python theme={null}
await mesa.bookmarks.create(
repo="my-repo",
name=f"session/{session_id}",
change_id=repo.head_change_id,
)
```
```bash CLI theme={null}
mesa bookmark create "session/$SESSION_ID" --repo acme/my-repo --revision main
```
Recommended naming: `session/` or `proposal/-`.
### Checkpoint-per-prompt
Create a new change (and move the session bookmark to it) after each user prompt. This gives you a clean timeline of intent, makes undo/redo straightforward, and lets you restore the repository to a known good state if a later step goes wrong.
```typescript TypeScript theme={null}
const change = await mesa.changes.create({
repo: "my-repo",
base_change_id: sessionBookmark.change_id,
message: `prompt: ${userPrompt}`,
authors: [{ name: "Agent", email: "agent@acme.dev" }],
files: agentEdits,
});
// To accept the changes, move the bookmark to the new change, making it the new head of the timeline.
await mesa.bookmarks.move({
repo: "my-repo",
bookmark: sessionBookmark.name,
change_id: change.id,
});
```
```python Python theme={null}
change = await mesa.changes.create(
repo="my-repo",
base_change_id=session_bookmark.change_id,
message=f"prompt: {user_prompt}",
authors=[{"name": "Agent", "email": "agent@acme.dev"}],
files=agent_edits,
)
# To accept the changes, move the bookmark to the new change, making it the new head of the timeline.
await mesa.bookmarks.move(
repo="my-repo",
bookmark=session_bookmark.name,
change_id=change.id,
)
```
To undo the last prompt, move the bookmark back to the previous change.
Recommended for:
* conversational coding agents,
* iterative content generation,
* any workflow where users expect "undo last prompt" behavior.
### Short-lived bookmarks
Keep session/proposal bookmarks temporary. Merge approved work quickly and delete abandoned bookmarks. Short-lived bookmarks reduce drift from `main`, lower conflict rates, and make reviews easier because diffs stay small.
```typescript TypeScript theme={null}
// Delete an abandoned bookmark
await mesa.bookmarks.delete({ repo: "my-repo", bookmark: `session/${sessionId}` });
```
```python Python theme={null}
# Delete an abandoned bookmark
await mesa.bookmarks.delete(repo="my-repo", bookmark=f"session/{session_id}")
```
Good rule of thumb: if a bookmark has been idle for a while, either merge it into `main` or delete it.
### Approvals (proposal bookmark + diff)
Use a proposal bookmark for all non-trivial changes, then show users a diff before merge:
1. Agent writes changes on a proposal bookmark.
2. Fetch the diff between `main` and the proposal bookmark.
3. Render the diff in your UI in a way that makes sense for your app.
4. On user approval, merge the proposal into `main`.
5. On reject, keep the bookmark for iteration or delete it.
This pattern gives users explicit control and creates a clear audit trail of what was proposed and accepted.
```typescript TypeScript theme={null}
// 1. Create a proposal bookmark at the current main
const bookmarks = await mesa.bookmarks.list({ repo: "my-repo" });
const main = bookmarks.bookmarks.find((bookmark) => bookmark.name === "main")!;
await mesa.bookmarks.create({
repo: "my-repo",
name: "proposal/add-forecast-widget",
change_id: main.change_id,
});
// 2. Agent writes changes on the proposal bookmark (via the filesystem or changes API),
// advancing it to `proposalHead`.
// 3. Diff main vs. the proposal
const diff = await mesa.diffs.get({
repo: "my-repo",
base_change_id: main.change_id,
head_change_id: proposalHead,
});
renderApprovalUI(diff);
if (userApproved) {
// 4. Merge the proposal into main
await mesa.bookmarks.merge({
repo: "my-repo",
target: "main",
source: "proposal/add-forecast-widget",
authors: [{ name: "Agent", email: "agent@acme.dev" }],
delete_source: true,
});
} else {
// 5. Clean up (if you didn't use the `delete_source` option in the merge)
await mesa.bookmarks.delete({
repo: "my-repo",
bookmark: "proposal/add-forecast-widget",
});
}
```
```python Python theme={null}
# 1. Create a proposal bookmark at the current main
bookmarks = await mesa.bookmarks.list(repo="my-repo")
main = next(bookmark for bookmark in bookmarks.bookmarks if bookmark.name == "main")
await mesa.bookmarks.create(
repo="my-repo",
name="proposal/add-forecast-widget",
change_id=main.change_id,
)
# 2. Agent writes changes on the proposal bookmark (via the filesystem or changes API),
# advancing it to proposal_head.
# 3. Diff main vs. the proposal
diff = await mesa.diffs.get(
repo="my-repo",
base_change_id=main.change_id,
head_change_id=proposal_head,
)
render_approval_ui(diff)
if user_approved:
# 4. Merge the proposal into main
await mesa.bookmarks.merge(
repo="my-repo",
target="main",
source="proposal/add-forecast-widget",
authors=[{"name": "Agent", "email": "agent@acme.dev"}],
delete_source=True,
)
else:
# 5. Clean up (if you didn't use the delete_source option in the merge)
await mesa.bookmarks.delete(
repo="my-repo",
bookmark="proposal/add-forecast-widget",
)
```
# General
* Treat `main` as canonical state, not a scratchpad.
* Add metadata in commit messages so runs are traceable.
* Keep API/JWT scopes as tight as possible
* Track write, diff, and merge latency in your critical paths.
* Enforce repository naming conventions and lifecycle policies.
* Delete abandoned proposal bookmarks after a retention window.
* Keep human review workflows for high-impact changes.