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

# YAML Reference

> Every key a workspace config accepts, and what each one does.

## What It Does

A workspace can be described entirely in YAML and loaded with one call — `load_config` in Python, `loadWorkspaceConfigFile` in TypeScript. This page is the key-by-key reference. Both languages read the same document and refuse the same things: an unknown key is an error, not a warning, so a typo fails loudly at load rather than silently doing nothing.

## Top level

| Key | Type | Default | Meaning |
| - | - | - | - |
| `mounts` | map | required | Prefix to mount block. See below. |
| `mode` | `read` \| `write` \| `exec` | `write` | The mode a mount inherits when it declares none. |
| `read` | `fresh` \| `bounded` | `bounded` | The read policy a mount inherits when it declares none. There is deliberately no top-level `ttl:` — see [the bound](#the-bound). |
| `command_limits` | map | — | Per-command caps for every session. See [command limits](#command-limits). |
| `clis` | map | — | Installed CLIs, keyed by head word. |
| `runtimes` | list | — | The workspace's ordered runtime world. |
| `profiles` / `profile` | map / string | — | Permission documents, and the default one. |
| `route_policy` | path | — | A `.py` whose last expression names the runtime for a line. |
| `cache` / `index` / `store` / `console` | block | — | Where bytes, listings, workspace state and job output live. |
| `env` / `secrets` | map | — | The environment plane and the source table. |
| `default_session_id` / `default_agent_id` / `workspace_id` | string | — | Identity. |

## Mount block

| Key | Type | Default | Meaning |
| - | - | - | - |
| `vfs` | string | required | The registered backend name, or a `./file.py:Class` reference. |
| `config` | map | `{}` | The backend's own config, validated by that backend's model. A key the model does not declare is refused by name (`linear: team_idz: ...`), never ignored. |
| `mode` | `read` \| `write` \| `exec` | top-level `mode` | This mount's mode. |
| `read` | `fresh` \| `bounded` | top-level `read` | How this mount's cached bytes are served. |
| `ttl` | integer (seconds) | `600` | The staleness bound for this mount's cached bytes and cached listings, under both `bounded` and `fresh`. |
| `index` | block | top-level `index` | This mount's own listing store, the top-level `index:` keys (`type`, `ttl`, `url`, `key_prefix`). It replaces the top-level block whole; see [a mount's own index](#a-mounts-own-index). |
| `command_limits` | map | `{}` | Per-command caps for this mount. |
| `backend` | `workspace` \| `fuse` \| `fskit` | `workspace` | Whether the mount also registers a real mountpoint. |
| `mountpoint` | path | — | Where, for the kernel backends. |

```yaml theme={null}
mode: write
read: bounded

mounts:
  /fast:
    vfs: s3
    config: {bucket: reports}
    read: bounded
    ttl: 600
  /live:
    vfs: s3
    config: {bucket: feed}
    read: fresh
  /scratch:
    vfs: ram
```

## Command limits

`cat`, `grep`, `rg`, `head`, and `tail` cap their output at 2000 lines, and
every command stops after 600 seconds. Change that with `command_limits` in
three places: at the top level for every session, on a mount for commands
that run on it, and on a profile for sessions created with it:

```yaml theme={null}
command_limits:
  head:
    max_lines: 500
    timeout_seconds: 30
mounts:
  /data:
    vfs: ram
    mode: WRITE
    command_limits:
      grep:
        max_lines: 50
        on_exceed: error
profiles:
  researcher:
    command_limits:
      head:
        max_lines: 5000
```

A command's limit comes from the first place that names it: the session's
profile, the mount the command runs on, the workspace, then the built-in
default. A command that spans several mounts takes the tightest of their
limits. An entry replaces that command's whole limit, so restate
`timeout_seconds` if you still want a deadline; commands it does not name keep
their defaults.

* `max_lines` / `max_bytes` cap the output; `null` means no cap.
* `timeout_seconds` is a deadline; a command that runs past it exits `124`.
* `on_exceed: truncate` (default) keeps the capped output, adds a stderr
  notice, and leaves the exit code alone. `on_exceed: error` drops the output
  and exits `1`, so `&&` and `||` see the failure.

Caps apply to what each command prints, one command at a time:
`cat big.txt; echo end` still prints `end`. Data going into a pipe, a
redirect, or `$(...)` is never cut, so `cat big.txt | wc -l` counts every
line. See [Output Limits](/python/quickstart#output-limits) for the SDK form.

## The read policy

`read: bounded` serves cached bytes and cached directory listings without asking the backend, for as long as `ttl:` allows. `read: fresh` revalidates against the backend's content token before serving a cached copy, at the cost of one backend stat per file read, and checks a cached listing before serving it: against the listing's stored version where the backend has one, otherwise by re-listing the folder once per command. See [listings under `fresh`](/home/cache#listings-under-fresh).

`fresh` is refused at mount time on a backend that cannot honour it, rather than quietly behaving as `bounded`:

* the backend caches neither reads nor listings, so the check has nothing to run against (`ram`, `redis`, `postgres`, `mongodb`, a local `lancedb`). A backend that caches listings but no bytes (`disk`, `chroma`, `qdrant`, `airtable`, `wandb`) accepts `fresh`, because its listings are checked, and so does any mount whose `index:` (its own or the workspace's) keeps listings for a nonzero `ttl`;
* or it caches reads but stamps no token the check can compare, because its `stat` and its read return different kinds of value, or its read stamps nothing.

[The VFS matrix](/home/vfs-matrix) lists which backends accept `fresh` today.

### The bound

A bare `ttl:` lives only in a mount block. At the top level it would sit beside `index: {ttl: ...}` and mean a different thing, so the workspace-level default is `read:` alone and a workspace-level `bounded` takes 600 seconds. Inside a mount the two are told apart by nesting: `ttl:` is the bound, and a mount's own `index: {ttl: ...}` is how long its listing store keeps a listing, never longer than that bound.

Each listing write is capped by the mount's `ttl`, under both policies. Backends may expire listings sooner, and mounts without a listing cache still keep none. `fresh` also checks listings, so a file added or removed outside mirage shows in the next `ls`, `find` or glob. A cached listing is served when:

1. the running command wrote it;
2. the mount is a GitHub mount pinned to a full commit sha and the listing was fetched at it, with no request (a Hugging Face Hub mount never pins);
3. its stored version still matches the backend's: one check per command for a GitHub or Hub mount, one local stat per folder on disk.

Otherwise the folder is listed again, and that re-list is trusted for the rest of the command. A read that belongs to no command (FUSE, a programmatic op such as `ws.vfs`, an agent's file tools) trusts a listing written, or a version check sent, in the last second, so calls in a short `ls -l` burst over FUSE can share one re-list or one check; a burst lasting beyond the window can re-list again, and an outside change can stay hidden from those reads for up to that second.

Disk versions each folder by its change time, which assumes a local POSIX filesystem. For a network or FUSE root (NFS, SMB, rclone, s3fs, mirage's own FUSE), whose folder change time may not move when its entries do, turn the versions off in the mount's `config:`; that mount then re-lists once per command:

```yaml theme={null}
mounts:
  /share:
    vfs: disk
    config: {root: /mnt/nfs/share, folder_versions: false}
    read: fresh
```

Existing listings in a persisted Redis index retain their stored expiry until rewritten.

### A mount's own index

A mount's `index:` block takes the top-level `index:` keys and replaces that block for this mount, whole: nothing is inherited from it, so `type:` is required, a missing `ttl:` is 600 seconds, and a Redis store's `url:` and `key_prefix:` take the block's own defaults, not the top-level ones. A mount without one takes the top-level `index:`, and with neither, the backend's own listing lifetime ([Index TTL](/home/cache#index-ttl)). `index: null` is the same as leaving it out. The file cache has no per-mount block; a mount's `cache:` is refused.

```yaml theme={null}
index: {type: redis, url: redis://cache:6379/0, ttl: 600}

mounts:
  /drive:
    vfs: gdrive
    read: bounded
    ttl: 86400
    index: {type: ram, ttl: 86400}   # this mount's listings, in RAM, for a day
  /work:
    vfs: disk
    config: {root: /srv/work}
    index: {type: ram, ttl: 0}       # this mount caches no listings
```

The mount's `ttl:` still caps every listing write, so a mount `index: {ttl: ...}` can only shorten how long a listing lives; `/drive` keeps listings for a day because its `ttl:` is raised to match. `fresh` is judged on the mount's own index: a zero `ttl` there takes listing-only `fresh` away from `disk` even under a nonzero top-level `index:`.

Two shapes are refused, because a `read`/`ttl` pair that disagrees is almost always a typo:

* `ttl:` with no `read:` — the bound pins nothing.
* `read: bounded` written out with no `ttl:` — an explicit policy with an implicit bound.

An unset `read:` is a different thing and is fine: it takes the default.

### Across a snapshot

A snapshot records each mount's policy and bound, and a restore keeps them --
but only for a mount the loader rebuilds from the saved state itself. A mount
handed back through `mounts=` takes the default (`bounded`, 600s) instead.

That is deliberate: a mount saved with redacted credentials *has* to be handed
back, and what comes back may be a different backend entirely. Replaying a saved
`fresh` onto one that cannot revalidate would refuse a restore that had nothing
wrong with it. The saved policy still has to be a policy mirage knows -- a
snapshot naming an unknown one is refused whether or not the mount is overridden.

Declare `read:` again on an overridden mount if you want it back.

### What the bound promises

The bound is stamped on a cache entry when that entry is written, and the store
expires it from there. So the bound that applies to an entry is the one the
mount that *wrote* it declared, not the one the mount reading it declares. Within
a workspace those are the same mount, so `ttl:` means what it says.

They can differ in three situations, and in each the older bound wins until the
entry expires on its own:

* two workspaces sharing one Redis cache declare different `ttl:` for the same
  prefix;
* `ttl:` is lowered and the process restarts against a surviving shared cache;
* a snapshot is restored into a mount whose `ttl:` differs from the one that
  took the snapshot.

Making the reader's bound authoritative would mean recording when each entry was
written somewhere every store can read it back — Redis keeps no such timestamp
today. Until then, treat a shared cache as a place where `ttl:` should agree
across the workspaces that use it.

## Not yet accepted

`read: pinned` is refused, naming the layer it needs: pinning reads to the content a commit records requires a version layer mirage does not have. There is no `write:` key yet. Both are reserved rather than silently ignored, so a config that names one fails at load instead of appearing to work.

## Time-scoped chat and calendar mounts

The `slack`, `discord`, and `gcal` backend config blocks accept `start_time`
and `end_time`: RFC3339 timestamps with a timezone and at most millisecond
precision. Start is inclusive and end is exclusive; either may be omitted or
null. An empty or reversed interval is refused. The same snake\_case fields
work through `build_vfs` in Python and `buildVfs` in TypeScript.

```yaml theme={null}
mounts:
  /calendar:
    vfs: gcal
    mode: read
    config:
      access_token: "<google-access-token>"
      time_zone: UTC
      start_time: "2026-06-01T00:00:00Z"
      end_time: "2026-07-01T00:00:00Z"
```

Chat selects messages by creation time; attachments follow those messages.
Calendar selects events overlapping the interval and retains their original
payloads, including events that cross a bound. Only day directories intersecting
the interval are exposed. Calendar day names use the mount's `time_zone` (or its
primary calendar's timezone); chat day names use UTC. Context such as users,
channels, and `calendar.json` remains available.

Calendar's bare listing includes all past event days. Without `end_time`, future
discovery stops at the end of today +90 days; explicit date paths/globs can reach
further within the configured scope. With `end_time`, that explicit endpoint
replaces the default horizon. Calendar refuses a listing still incomplete after
50 API pages; narrow the range instead of receiving a silently truncated result.

A `gcal` block also takes `bucket_days`: `1` (the default), `7` or `30`, the days
each date directory covers. With `7` or `30` a directory is named by its first
and last day, `2026-08-10--2026-08-16/`, on a fixed grid where 7-day buckets run
Monday to Sunday, and each event file name carries its day
(`<eventId>__2026-08-11_0900-1030_<Title>.gcal.json`). One listing then answers a
week or a month of events. Other values are refused.

```yaml theme={null}
mounts:
  /calendar:
    vfs: gcal
    mode: read
    config:
      access_token: "<google-access-token>"
      bucket_days: 7
```

Without either bound, Slack and Discord retain their recent listing defaults
(90 and 30 days respectively). Configuring a bound makes the selected interval
discoverable. Bounds are enforced on reads, metadata, listings, and VFS searches;
separately installed account CLIs retain their own access. Read-only does not
freeze upstream changes. Change bounds by rebuilding/remounting the VFS, which
clears its cached state.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.