Skip to main content
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 and the 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:
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 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 for the definition’s other operations):
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).
layout.json
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

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:
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.
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.