> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mesa.dev/llms.txt
> Use this file to discover all available pages before exploring further.

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

<CodeGroup>
  ```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")
  ```
</CodeGroup>

`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"`.

<Tip>
  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.
</Tip>

## 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):

<CodeGroup>
  ```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:
      ...
  ```
</CodeGroup>

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

<CodeGroup>
  ```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},
  )
  ```
</CodeGroup>

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.

<Warning>
  In the sandbox environment, set `MESA_ACCESS_TOKEN`. Its issuer determines the organization for the layout mount.
</Warning>

`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).
