> ## 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 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](/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(...)` and map absolute paths to declarations. 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({ apiKey: process.env.MESA_API_KEY });

  const fs = await mesa.fs.mount({
    layout: {
      "/workspace": repo("my-app", {
        mode: "rw",
        bookmark: "main",
        subPaths: {
          ".agents/skills": [
            repo("code-review-skill", { mode: "ro", alias: "code-review" }),
            repo("release-notes-skill", { mode: "ro", alias: "release-notes" }),
          ],
        },
      }),
    },
  });

  // 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(api_key=os.environ["MESA_API_KEY"])

  async with mesa.fs.mount(
      layout={
          "/workspace": repo(
              "my-app",
              mode="rw",
              bookmark="main",
              sub_paths={
                  ".agents/skills": [
                      repo("code-review-skill", mode="ro", alias="code-review"),
                      repo("release-notes-skill", mode="ro", alias="release-notes"),
                  ],
              },
          ),
      },
  ) 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 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>
  `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.
</Tip>

## 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](#organization-resolution)).

```json layout.json theme={null}
{
  "/workspace": {
    "kind": "repo",
    "name": "my-app",
    "mode": "rw",
    "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" }
      ]
    }
  }
}
```

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

| 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`.            |
| `bookmark` | No       | Bookmark to check out. Mutually exclusive with `changeId`.                      |
| `changeId` | No       | Change ID to check out. Mutually exclusive with `bookmark`.                     |
| `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 `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`:

```bash theme={null}
MESA_ORG=my-org MESA_API_KEY=mesa_... 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 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.

<CodeGroup>
  ```typescript TypeScript theme={null}
  const definition = await mesa.fs.define({
    "/workspace": repo("my-app", { mode: "rw", bookmark: "main" }),
  });

  // In your backend: mint the layout-scoped token
  const { token } = await definition.getToken({ ttl: 3600 });

  // In the sandbox: write the layout file and mount with the token
  sandbox.writeFile("layout.json", definition.layout.toString());
  sandbox.exec("mesa mount --layout=layout.json --daemonize", {
    env: { MESA_ORG: "my-org", MESA_ACCESS_TOKEN: token },
  });
  ```

  ```python Python theme={null}
  definition = await mesa.fs.define(
      {"/workspace": repo("my-app", mode="rw", bookmark="main")}
  )

  # In your backend: mint the layout-scoped token
  token = (await definition.get_token(ttl=3600)).token

  # In the sandbox: write the layout file and mount with the token
  sandbox.write_file("layout.json", str(definition.layout))
  sandbox.exec(
      "mesa mount --layout=layout.json --daemonize",
      env={"MESA_ORG": "my-org", "MESA_ACCESS_TOKEN": token},
  )
  ```
</CodeGroup>

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.

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

`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](/content/reference/mesa-cli).
