Skip to main content
A layout composes a mount’s namespace from your repositories: each repository appears at a path you declare, including nested inside another repository’s tree. A layout replaces the default organization browse tree entirely — the mount contains exactly the paths the layout declares, and the canonical /<org>/<repo> paths are not present. Layouts work with both the app mount and the POSIX mount (mesa mount --layout).

Mounting a layout from the SDK

Declare each repository with repo(...) and map absolute paths to declarations. This example mounts an application repository at /workspace with two read-only skills repositories nested inside its tree:
mode 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".
mount accepts either a raw path map (as above) or a prepared layout from mesa.fs.createLayout(paths) (TypeScript) / mesa.fs.create_layout(paths) (Python). Either way the declarations are checked as they are built — an empty name, a missing mode, a conflicting revision, or a non-absolute path fails where the layout is defined, and the remaining structural rules are enforced before anything mounts.

The layout file

A layout serializes to JSON — the format 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 never names an organization; the mount that consumes it supplies one (see Organization resolution).
layout.json
The SDK produces exactly this form: layout.toString() in TypeScript, str(layout) 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

When neither bookmark nor changeId 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:
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 resolution

The layout file never names an organization, so the mount needs exactly one from its context:
  • The SDK resolves every repository name within the client’s organization.
  • The CLI uses the organization MESA_ORG selects; without MESA_ORG, its single configured organization.
If the CLI has no organization configured, or several are configured (for example via MESA_ORGS) and MESA_ORG does not select one, the mount fails with an error pointing at MESA_ORG. Every repository in the layout must belong to that one organization; cross-org layouts are not supported.

Running the mount elsewhere

A common shape: your backend composes the layout and holds the API key, while the mount runs in a sandbox that should only ever see a scoped, short-lived credential. mesa.fs.define bundles a layout with the operations that flow needs — the layout itself serializes to layout.json, and getToken() / get_token() mints the layout’s least-privilege access token.
The same definition also mounts in-process: await definition.mount() in TypeScript, async with definition.mount() as fs: in Python. To mint a token without a definition, call mesa.fs.createToken(layout, { ttl }) / mesa.fs.create_token(layout, ttl=...) directly. The token helpers validate the layout against the structural rules above before minting, so a structurally invalid layout fails at token time with the same error the mount would report. Repository names are not resolved until mount time — a layout naming a nonexistent repository still mints a token and fails when the mount resolves it.
In the sandbox environment, set only MESA_ORG and MESA_ACCESS_TOKEN, and leave both MESA_API_KEY and MESA_ORGS unset. Each outranks MESA_ACCESS_TOKEN as a credential, so a leftover value would silently replace the scoped token you minted.
ttl is the token lifetime in seconds. API-key clients default to one hour and allow up to 24 hours; TypeScript 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.
  • mode is enforced per repository: 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.