# Architecture Source: https://docs.mirage.strukto.ai/home/architecture Four layers, one filesystem. How Mirage turns mounted services into one bash-driven environment.
Mirage architecture Mirage architecture
Mirage stacks four thin layers between an agent and the services it touches. ## 1. AI Agent and Application The agent (or any application embedding Mirage) issues bash commands, VFS calls, or syscalls. One vocabulary, every backend. ## 2. Mirage Bash and VFS The action surface. **Mirage Bash** parses commands with tree-sitter and runs them against the **Mirage VFS**, a unified filesystem API over every mount. A **FUSE Adapter** exposes the same tree to host tools when you want it. A **Command Registry** and **VFS Registry** describe what verbs and resources are available. ## 3. Dispatcher & Cache The **Mirage Dispatcher** routes each operation to the mount that owns the path, joining pipelines that span systems. The **Index & File Cache** absorbs repeats: the first directory walk hits the API, the next serves from cache; the first read streams bytes, later reads are local. ## 4. Infrastructure and Remote Whatever you mount: RAM, Disk, Redis, S3 / R2 / GCS / OCI / Supabase, Gmail / GDrive / GDocs / GSheets / GSlides, GitHub / Linear / Notion / Trello, Slack / Discord / Email, MongoDB / Postgres / LanceDB / Qdrant, SSH, and more. Each speaks the same filesystem semantics from the agent's point of view. Browse the [Resource Matrix](/home/resource-matrix) for the full list. ## Where to go next * [Resource Matrix](/home/resource-matrix) to pick a backend to mount. * [Python Quickstart](/python/quickstart) for working code in minutes. * [TypeScript Quickstart](/typescript/quickstart) for the same Workspace API in Node, browser, or edge. # Daemon Auth Source: https://docs.mirage.strukto.ai/home/auth How the Mirage daemon authenticates HTTP clients in local, token, and JWT modes. ## What It Does The daemon serves a local HTTP API on `127.0.0.1:8765`. Every request except `/v1/health` must present a bearer token. Which token is accepted is decided by `MIRAGE_AUTH_MODE`: | Mode | When to use | What the daemon accepts | | ----------------- | ------------------------------------------------------- | ------------------------------------------------------------------------- | | `local` (default) | One user, one machine. Zero config. | A random token the CLI mints into `~/.mirage/auth_token` (mode 0o600). | | `token` | Shared daemon, operator-issued PAT. | The exact string in `MIRAGE_AUTH_TOKEN`. | | `jwt` | Multi-tenant, external issuer (Clerk, Auth0, your own). | Any RS256-signed JWT that verifies against `MIRAGE_JWT_PUBKEY` / `_FILE`. | `/v1/health` is always reachable without a token so load balancers and process supervisors can probe it. ## Local Mode (Default) You usually do nothing. The first time the CLI spawns the daemon it writes a random 32-byte token to `~/.mirage/auth_token` at mode 0o600 and uses it on every subsequent request. ```bash theme={null} mirage workspace create workspace.yaml --id demo cat ~/.mirage/auth_token # the token the daemon expects ``` Probe it directly: ```bash theme={null} TOKEN=$(cat ~/.mirage/auth_token) curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8765/v1/workspaces # 200 curl http://127.0.0.1:8765/v1/workspaces # 401 curl http://127.0.0.1:8765/v1/health # 200 ``` ## Token Mode (Operator PAT) For a daemon you run yourself (Docker, systemd, a shared dev box), pin one token across all clients. ```bash theme={null} export MIRAGE_AUTH_MODE=token export MIRAGE_AUTH_TOKEN= # Python python -m uvicorn mirage.server.daemon:app --host 127.0.0.1 --port 8765 # TypeScript node typescript/packages/cli/dist/bin/daemon.js ``` Clients pass the same value: ```bash theme={null} curl -H "Authorization: Bearer $MIRAGE_AUTH_TOKEN" http://127.0.0.1:8765/v1/workspaces ``` Both server and CLI use a constant-time compare, so timing leakage is not a concern. ## JWT Mode (External Issuer) Hand the daemon a public key and it will accept any non-expired JWT signed by the matching private key. Verification is networkless: no JWKS fetch, no callback to the issuer. ```bash theme={null} export MIRAGE_AUTH_MODE=jwt export MIRAGE_JWT_PUBKEY_FILE=/etc/mirage/issuer-pub.pem export MIRAGE_JWT_ALG=RS256 # Optional hardening export MIRAGE_JWT_ISSUER=https://your-issuer.example export MIRAGE_JWT_AUDIENCE=mirage-daemon export MIRAGE_JWT_AUTHORIZED_PARTIES=https://app.example,https://cli.example export MIRAGE_JWT_CLOCK_SKEW_SECONDS=5 ``` Hard rules the daemon enforces: * `alg` is pinned to `MIRAGE_JWT_ALG`. A token signed with a different algorithm is rejected, which defeats alg-confusion attacks. * `alg=none` is always rejected. * `exp` is mandatory. * `typ`, if present, must be `JWT`. * Opaque (non-three-segment) values in `Authorization: Bearer` are rejected before key work, so probing is cheap. ## Environment Reference | Variable | Modes | Purpose | | ------------------------------- | ------------ | ---------------------------------------------- | | `MIRAGE_AUTH_MODE` | all | `local` (default), `token`, or `jwt`. | | `MIRAGE_AUTH_TOKEN` | local, token | Local-mode override; required in `token` mode. | | `MIRAGE_JWT_PUBKEY` | jwt | PEM string of the public key. | | `MIRAGE_JWT_PUBKEY_FILE` | jwt | Path to a PEM file (alternative to inline). | | `MIRAGE_JWT_ALG` | jwt | Signing algorithm to pin, e.g. `RS256`. | | `MIRAGE_JWT_ISSUER` | jwt | Required `iss` claim. | | `MIRAGE_JWT_AUDIENCE` | jwt | Required `aud` claim. | | `MIRAGE_JWT_AUTHORIZED_PARTIES` | jwt | Comma-separated allow-list for `azp`. | | `MIRAGE_JWT_CLOCK_SKEW_SECONDS` | jwt | Default `5`. | ## Where to Go Next * [CLI](/home/cli) walks the daily Workspace flow that uses local-mode automatically. * [Architecture](/home/architecture) shows where the auth middleware sits in the request path. # Bash Source: https://docs.mirage.strukto.ai/home/bash Run bash commands with `execute()`, per-call `cwd`/`env` overrides, and mid-flight cancellation. Mirage Bash is how agents act on the workspace. `execute()` parses a bash-style command, looks up the target session, resolves mounts, runs the executor, applies I/O side effects, and records history through the [Observer](/home/observer). ## Per-call overrides: `cwd`, `env` Providing `cwd` or `env` runs the command in an ephemeral session clone, like a bash subshell `(cd /data && cmd)`. Mutations like `cd` or `export` inside the call do NOT persist back to the workspace's session. To change persistent state, run the command without these options. ```python theme={null} # Persistent mutation (no options): like `cd /data; cmd` await ws.execute("cd /data") await ws.execute("ls") # sees /data # One-shot subshell (with cwd): like `(cd /data && cmd)` await ws.execute("ls", cwd="/data") # ws.cwd is unchanged; mutations inside don't leak ``` ```typescript theme={null} // Persistent mutation await ws.execute("cd /data") await ws.execute("ls") // sees /data // One-shot subshell await ws.execute("ls", { cwd: "/data" }) // ws.cwd is unchanged; mutations inside don't leak ``` ```bash theme={null} # Use a real bash subshell inside the command string mirage execute -w demo -c "(cd /data && ls)" mirage execute -w demo -c "(export FOO=bar; printenv FOO)" ``` The CLI doesn't have `--cwd` / `--env` flags, but bash subshell syntax `(cd ... && cmd)` and `(export FOO=bar; cmd)` give the same per-call isolation. Mutations inside the parens don't leak. This makes per-call overrides safe under concurrent calls on the same session. Two parallel `execute()` calls with different `cwd` see their own cwd without cross-contamination, even on the same session. ## Subshells `(...)` Wrapping commands in `( ... )` runs them in an isolated copy of the session: `cd`, `export`, and other mutations inside the parens do not leak back. It is the same isolation as the `cwd` / `env` overrides above, and the CLI's stand-in for them (there are no `--cwd` / `--env` flags). ```bash theme={null} (cd /data && ls) # cwd change scoped to the subshell (export TOKEN=xyz; printenv) # env var gone once the parens close ``` The isolation holds under concurrency, and subshells are still covered by the per-session mount allowlist and the cancellation boundaries below. ## Mid-flight cancellation: `cancel` / `signal` Both bindings support cooperative cancellation observed at recursion boundaries (LIST, PIPELINE, FOR/WHILE/UNTIL iterations, COMMAND, subshells, command substitution) and inside `sleep`. On cancel, the call raises an abort error. ```python theme={null} import asyncio from mirage.workspace.abort import MirageAbortError cancel = asyncio.Event() async def trigger(): await asyncio.sleep(0.1) cancel.set() asyncio.create_task(trigger()) try: await ws.execute("sleep 5", cancel=cancel) except MirageAbortError: print("aborted") ``` ```typescript theme={null} try { await ws.execute("sleep 5", { signal: AbortSignal.timeout(100) }) } catch (e) { if (e instanceof DOMException && e.name === "AbortError") { console.log("aborted") } } ``` ```bash theme={null} # Background the job, then cancel it JOB_ID=$(mirage execute -w demo -c "sleep 60" --bg) mirage job cancel "$JOB_ID" ``` Per-call timeout is not a CLI flag yet. Use `--bg` to get a job id and `mirage job cancel` to terminate, or wrap the command in the `timeout` builtin: `mirage execute -w demo -c "timeout 30 "` exits `124` on overrun. ## Three Scopes for State | Need | API | Bash equivalent | | ------------------------------------------- | ---------------------------------------- | ------------------- | | One isolated command | `execute(cmd, cwd=..., env=...)` | `(cd /data && cmd)` | | Many isolated commands sharing scoped state | `session_id=...` (Py) / `sessionId` (TS) | a separate terminal | | Persistent shell mutations | run without options | `cd /data; cmd` | ## JSON with `jq` `jq` reads a **stream of JSON values** and runs the program once per value, matching the real `jq` binary. The filename is irrelevant: a `.json` file holding several concatenated or pretty-printed values is a stream just like a `.jsonl` file, and so is multi-document input arriving on stdin. ```bash theme={null} # Two documents in one file -> the program runs twice, one line each cat /data/events.json {"id": 1} {"id": 2} jq -c '.id' /data/events.json 1 2 # -s slurps the whole stream into a single array first jq -c -s 'map(.id)' /data/events.json [1,2] ``` Because evaluation is per document, commands that emit newline-delimited JSON (such as [paginated `gws` list calls](/python/resource/gdrive#pagination)) pipe straight into `jq` with no reshaping. Output arity follows the program, not the input. A jq program emits a stream of values and each one prints on its own line, so `.a[]`, `.a, .b` and `range(3)` all print several lines, while a program that collects into an array (`[.a[] | .t]`) emits one value and prints one line. ```bash theme={null} jq -c '.name, .age' /data/user.json "alice" 30 jq -c '[.name, .age]' /data/user.json ["alice",30] ``` ### Flags | Reading input | | | ----------------------- | ---------------------------------------------------------- | | `-n`, `--null-input` | run once against `null`; `inputs` still reads the operands | | `-R`, `--raw-input` | each line is a string, not a JSON document | | `-s`, `--slurp` | one value for the whole stream, spanning every operand | | `--stream` | read each document as its `[path, leaf]` events | | `--seq` | read and write RFC 7464 sequences (RS before each value) | | `-f`, `--from-file` | read the program from a file | | `--arg name value` | bind `$name` to a string | | `--argjson name value` | bind `$name` to a JSON value | | `--rawfile name file` | bind `$name` to a file's text | | `--slurpfile name file` | bind `$name` to a file's documents, as an array | | `--args`, `--jsonargs` | read the remaining operands into `$ARGS.positional` | | Writing output | | | ------------------------------------------- | -------------------------------------------------------------------- | | `-r`, `--raw-output` | print string outputs unquoted | | `-j`, `--join-output` | `-r` with no separator | | `--raw-output0` | `-r` with a NUL after each output | | `-c`, `--compact-output` | one line per output | | `-a`, `--ascii-output` | escape non-ASCII (and keep strings quoted, as jq does) | | `-S`, `--sort-keys` | sort object keys | | `--tab`, `--indent n` | indent width (`--indent -1` is a tab) | | `-e`, `--exit-status` | exit 1 when the last output is `false`/`null`, 4 when there was none | | `-M`, `--monochrome-output`, `--unbuffered` | accepted; already how mirage writes | Build JSON with a binding rather than by hand: the value arrives as a value, so quotes and newlines in it need no escaping. ```bash theme={null} # text -> JSON array, no string surgery printf 'alpha\nbeta\n' > /data/lines.txt jq -Rn -c '[inputs]' /data/lines.txt ["alpha","beta"] # a shell value that contains quotes jq -n -c --arg v 'a"b' '{msg: $v}' {"msg":"a\"b"} # a whole file as one JSON string jq -n -c --rawfile body /data/lines.txt '{text: $body}' {"text":"alpha\nbeta\n"} ``` `$ARGS` is always defined, carrying `named` (the `--arg` family) and `positional` (`--args` / `--jsonargs`). Three limits worth knowing. `inputs` is bound to whatever is still unread, so a program that drains it (`[., inputs]`, `reduce inputs as $x`) runs once and sees everything, but the stateful single `input` and a partial drain (`first(inputs)`) are not modeled. `--stream` reads whole documents and expands them, which matches jq except that jq's incremental parser splits the closing event of an input with no trailing newline into its own `-s` group. And `--seq` reads and writes the separator, but drops text before the first one silently where jq names it on stderr. Not implemented, and reported as an unknown option rather than quietly ignored: `-C` (colorized output, which an agent would only have to strip again), `-L` (no module system, so `include` has nothing to search), `--stream-errors` (it reports truncated-parse errors, which whole-value reads never produce), and `--build-configuration`. ## Supported bash syntax Mirage Bash is a tree-sitter-bash parser plus a custom executor. It implements the constructs LLMs reach for most often. What is not supported returns a clear, parseable error so an agent can self-correct on its next turn. ### Supported * **Operators:** pipes `|`, `|&`; lists `&&`, `||`, `;`; background `&`. * **Redirects:** `>`, `>>`, `<`, `2>`, `2>&1`, `&>`, `&>>`, heredoc `<<`, herestring `<<<`. * **Substitutions:** command substitution `` `cmd` `` and `$(cmd)`; arithmetic `$((expr))`; parameter expansion `${VAR}`, `${VAR:-default}`, `${VAR%suffix}`, etc.; input-direction process substitution `<(cmd)`. * **Control flow:** `if`/`elif`/`else`/`fi`, `for`, `while`, `until`, `case`, `select`, `function name() {}`, `break`, `continue`, `return`. * **Grouping:** subshells `(cmd)`, compound `{ cmd; }`, negation `! cmd`. * **Builtins:** `cd`, `pwd`, `echo`, `printf`, `printenv`, `read`, `source`, `.`, `eval`, `export`, `unset`, `local`, `set`, `shift`, `trap` (no-op), `test`, `[`, `[[`, `true`, `false`, `sleep`, `xargs`, `timeout`, `bash`, `sh`, `python`, `python3`, `man`, `command`, `type`, `which`. * **Builtin options (GNU semantics):** `echo -n/-e/-E` (leading-word option rule: `echo hi -n` prints `hi -n`), `read -r`, `xargs -n/-0/-d/-r/--` (batching, GNU exit codes: `123` when an invocation fails, `126`/`127` stop the run), `timeout DURATION` with `s`/`m`/`h`/`d` suffixes (kills at the deadline with exit `124`, usage errors exit `125`). `shift` and `return` report bash's `numeric argument required` errors. * **Name lookup:** `type name` reports what a name resolves to (`type -t` prints one of `keyword`, `function`, `cli`, `builtin`; `type -a` lists every layer holding the name), `which name` prints the name of anything runnable (there is no PATH, so there is no path to print) and reports a miss through exit `1` alone, and `man name` renders a page: a command's spec, or an installed CLI's own `--help` tree (`man linear issue create`). * **Globs:** `*`, `?`, `[...]` classes and `[!...]` negation (Python `fnmatch` semantics in both implementations), resolved by the shell or pushed down to the resource. * **Comments:** `#`. ### Unsupported (returns clear error) * **Job control:** `bg`, `disown`. (`fg`, `jobs`, `wait`, `kill`, `ps` work; use the `--background` flag and `mirage job` CLI for long-running work.) * **Shell internals:** `exec`, `complete`, `compgen`, `ulimit`. * **Output process substitution:** `>(cmd)` (the `<(cmd)` direction works). * **Builtin options with no process backing:** `xargs -I`/`-P` (exit `1`) and `timeout -s`/`-k`/`--preserve-status` (exit `125`) return an `unsupported option` error: commands run as coroutines inside the workspace, so there is no process to signal or parallelize. Each returns `exit_code 2` with stderr `mirage: unsupported builtin: ` or `mirage: unsupported: process substitution >(...)`, except the builtin options above, which use the listed GNU-shaped exit codes. ### Syntax errors Commands the parser cannot make sense of return `exit_code 2` with stderr `mirage: syntax error near ''`. Earlier versions silently ran whatever fragment did parse; that no longer happens. ### What `--background` is and isn't The daemon's `--background` flag detaches a job and returns a job id. It is not the same as the bash `&` operator, which the shell does support inline (`sleep 30 &`). Use `&` for in-shell job parallelism, `--background` (or `mirage job`) for long-lived work that should outlive the request. ## Per-session mount modes A session can be created with its own per-mount modes, like a container that mounts the same volume `ro` while another mounts it `rw`. Each listed prefix carries a mode ceiling on the `read < write < exec` ladder, written as the words `read`/`write`/`exec` or the cumulative filesystem aliases `r`/`rw`/`rwx` (exec implies write implies read, so bit-style forms like a bare `w` are rejected). A command touching a mount the session was not given is rejected with `mirage: session 'agent' not allowed to access mount '/X'` and exit code 1; a command exceeding the session's mode fails exactly like it would on a read-only mount. If a session is created without `mounts`, it is unrestricted: every mount behaves per its own configured mode. A list of prefixes (instead of a mapping) restricts the session to those mounts but keeps each at its own mode, and a bare `-m /data` on the CLI does the same for one mount. A session's mode can only narrow, never widen: the effective permission is the weaker of the mount's own mode and the session's mode, so `rw` on a `READ` mount is still read-only. This is a soft boundary, enforced inside the daemon process, not an OS or process-level isolation. Use it to shrink the blast radius of prompt-injection in multi-agent workspaces: a Slack-only agent cannot pivot to read `/linear`, `/github`, or any other mount it was not given. Note that FUSE mounts are not scoped by session modes: a FUSE mount is a host-level surface served under the default unrestricted view (mount modes still apply, session narrowing does not). The check fires for every code path that reaches a mount: shell commands (`cat`, `ls`, ...), redirects (`>`, `<`), cross-mount `cp`/`mv`, `wget -O`, `curl -o`, command substitution `$(...)`, subshells `(...)`, pipes, `&&`/`||` chains, background jobs, and the programmatic `ws.ops.read/write/...` API. Infrastructure prefixes are always accessible: the history view (`/.bash_history`, which the `history` builtin and the GNU histfile render from) and the implicit scratch root (`/`, where stateless text-processing commands like `wc` resolve when given no path). A user-defined `/` mount is not infrastructure; sessions must be given `/` explicitly to touch it. ```python theme={null} ws = Workspace({ "/s3": s3, "/slack": slack, "/linear": linear, }) ws.create_session("slack-agent", mounts=["/slack"]) ws.create_session("data-agent", mounts={"/s3": "rw", "/github": "r"}) await ws.execute("ls /slack", session_id="slack-agent") # ok await ws.execute("cat /linear/issues/SEC-42", session_id="slack-agent") # exit_code=1, stderr=b"session 'slack-agent' not allowed to " # b"access mount '/linear'\n" ``` ```bash theme={null} # Repeat --mount (or -m) per allowed prefix; cap the mode with # :read/:write/:exec or the aliases :r/:rw/:rwx mirage session create demo --id slack-agent --mount /slack mirage session create demo --id data-agent -m /s3:rw -m /github:r mirage execute -w demo -s slack-agent -c "cat /linear/issues/SEC-42" # mirage: session 'slack-agent' not allowed to access mount '/linear' ``` The modes are a property of the session, so they cover every command issued under that `session_id`, including subshells, pipelines, and recursive `bash -c '...'`. They do not change the mount's own `MountMode`: a write to a session-writable mount is still rejected if the mount itself is `READ`. The two checks compose. ## Agent Pattern Agent harnesses commonly fan out tool calls in parallel, each with its own `cwd`/`env`/`cancel`. The clone semantics make this race-free without per-call boilerplate. From the CLI, a subshell per call gives the same isolation. ```python theme={null} async def tool_call(cmd: str, cwd: str, env: dict[str, str], timeout: float): cancel = asyncio.Event() asyncio.get_event_loop().call_later(timeout, cancel.set) return await ws.execute(cmd, cwd=cwd, env=env, cancel=cancel) results = await asyncio.gather( tool_call("ls", "/data", {"DEBUG": "1"}, 5.0), tool_call("grep foo *.log", "/logs", {"DEBUG": "1"}, 5.0), ) ``` ```typescript theme={null} async function toolCall( cmd: string, cwd: string, env: Record, timeoutMs: number, ) { return ws.execute(cmd, { cwd, env, signal: AbortSignal.timeout(timeoutMs) }) } const results = await Promise.all([ toolCall("ls", "/data", { DEBUG: "1" }, 5000), toolCall("grep foo *.log", "/logs", { DEBUG: "1" }, 5000), ]) ``` ```bash theme={null} # No --cwd/--env flags: isolate each parallel call in a subshell mirage execute -w demo -c "(cd /data && export DEBUG=1 && ls) & (cd /logs && export DEBUG=1 && grep foo *.log) & wait" ``` Each `( ... )` runs in its own scope, so the parallel `cd` / `export` don't collide. `&` backgrounds them inside Mirage Bash and `wait` joins. # Cache Source: https://docs.mirage.strukto.ai/home/cache The two-layer workspace cache, index and file, with RAM and Redis stores. ## What It Does Every `Workspace` ships with a **two-layer cache** so repeated work against remote backends (S3, GDrive, Slack, ...) hits local state instead of the network: * **Index cache.** Listings and metadata. The first directory walk hits the API; subsequent ones serve from the index until the TTL expires. * **File cache.** Object bytes. The first read streams from origin; later pipelines read from cache. ## Stores Each layer is a pluggable store with two built-ins: * **RAM** (default): in-process, zero setup, 512 MB file cache and 10-minute index TTL. Best for single-process apps and notebooks. * **Redis**: shared across workers, processes, and machines. Best for serverless, multi-replica services, or for cache state that survives restarts. ```python Python theme={null} from mirage import Workspace from mirage.cache.file.config import RedisCacheConfig from mirage.cache.index.config import RedisIndexConfig from mirage.resource.s3 import S3Config, S3Resource ws = Workspace( {"/s3": S3Resource(S3Config(bucket="my-bucket"))}, cache=RedisCacheConfig(url="redis://localhost:6379/0", limit="8GB"), index=RedisIndexConfig(url="redis://localhost:6379/0", ttl=600), ) ``` ```typescript TypeScript theme={null} import { S3Resource, Workspace } from '@struktoai/mirage-node' const ws = new Workspace( { '/s3': new S3Resource({ bucket: 'my-bucket' }) }, { cache: { type: 'redis', url: 'redis://localhost:6379/0', limit: '8GB' }, index: { type: 'redis', url: 'redis://localhost:6379/0', ttl: 600 }, }, ) ``` ## Eviction & Limits The two layers are bounded differently: | Layer | Holds | Default | Bound | Eviction | | --------------- | ---------------------------------------- | --------------- | -------------------------------------- | ---------------------------------------------------------------------- | | **File cache** | object bytes per virtual path | RAM, 512 MB | `cache_limit` (Py) / `cacheLimit` (TS) | LRU: least-recently-used bytes drop once the total exceeds the limit | | **Index cache** | directory listings + `FileStat` metadata | RAM, 10-min TTL | `ttl` (seconds) | time-based: entries expire after the TTL, then re-fetch on next access | Raising the file limit keeps more bytes warm at the cost of memory; lengthening the index TTL serves listings longer between API walks at the cost of staleness. ## Miss/Hit Lifecycle ```python Python theme={null} from mirage import Workspace from mirage.resource.s3 import S3Config, S3Resource ws = Workspace({"/s3": S3Resource(S3Config(bucket="my-bucket"))}) # 1. Index miss → S3 LIST. Listing stored in index cache. await ws.execute("ls /s3/data/") # 2. Index hit → 0 network calls. await ws.execute('find /s3/data/ -name "*.jsonl"') # 3. File miss → S3 GET. Bytes stored in file cache. await ws.execute("cat /s3/data/log.jsonl | wc -l") # 4. File hit → 0 network calls. await ws.execute("grep alert /s3/data/log.jsonl") ``` ```typescript TypeScript theme={null} import { S3Resource, Workspace } from '@struktoai/mirage-node' const ws = new Workspace({ '/s3': new S3Resource({ bucket: 'my-bucket' }) }) // 1. Index miss → S3 LIST. Listing stored in index cache. await ws.execute('ls /s3/data/') // 2. Index hit → 0 network calls. await ws.execute('find /s3/data/ -name "*.jsonl"') // 3. File miss → S3 GET. Bytes stored in file cache. await ws.execute('cat /s3/data/log.jsonl | wc -l') // 4. File hit → 0 network calls. await ws.execute('grep alert /s3/data/log.jsonl') ``` ## Relationship To Snapshots The file cache is exactly what a [snapshot](/home/snapshot) serializes: `ws.snapshot()` writes the cached bytes for every touched path into the tar, and `Workspace.load()` restores them into the file cache so a replayed run reads from local state. The index cache is not snapshotted; it rebuilds lazily after load. # CLI Source: https://docs.mirage.strukto.ai/home/cli Drive Mirage from the shell. Spin up a workspace from YAML, run commands against your mounts, snapshot and restore. The `mirage` CLI is a thin httpx wrapper over the Mirage daemon. It auto-spawns the daemon on first `workspace create`, and the daemon auto-exits 30 seconds after the last workspace is deleted. Most users never type a daemon command directly. Output is structured JSON to stdout for every verb -- pipe to `jq`, save to file, or read it directly. ## Install ```bash theme={null} curl -fsSL https://strukto.ai/mirage/install.sh | sh # or npm install -g @struktoai/mirage-cli # or uvx mirage-ai # or npx @struktoai/mirage-cli ``` Verify: ```bash theme={null} mirage --help ``` ## Define a workspace in YAML A workspace is a set of prefixed mounts plus some workspace-level settings. Save this as `workspace.yaml`: ```yaml theme={null} mode: WRITE mounts: /: resource: ram mode: WRITE /s3: resource: s3 mode: READ config: bucket: ${AWS_S3_BUCKET} region: ${AWS_DEFAULT_REGION} aws_access_key_id: ${AWS_ACCESS_KEY_ID} aws_secret_access_key: ${AWS_SECRET_ACCESS_KEY} ``` `${VAR}` placeholders are interpolated from your shell environment at `mirage workspace create` time. Missing vars fail fast with the full list, not lazily on first use. ## Walkthrough A guided tour from creating a workspace to snapshotting it. Each step builds on the previous one; you can copy them in order. For a runnable end-to-end version against a real multi-mount workspace (`/s3`, `/gdrive`, `/gmail`, `/slack`, `/discord`), see examples/python/cross/README.md. ### 1. Source env and create a workspace The YAML's `${...}` placeholders resolve from your shell at create time, so source your env first. The daemon auto-spawns on the first `create`. ```bash theme={null} set -a && source .env.development && set +a mirage workspace create workspace.yaml --id demo ``` ### 2. Inspect `list` is one line per workspace; `get` returns the full mount and session detail. ```bash theme={null} mirage workspace list mirage workspace get demo ``` ### 3. Run commands against your mounts `execute` runs a shell command inside the workspace. Paths resolve through the mount registry (`/s3/...` hits S3, `/` hits the RAM backing, etc.). ```bash theme={null} mirage execute --workspace_id demo --command "ls /s3/" mirage execute --workspace_id demo --command "head -n 1 /s3/data/example.jsonl" ``` ### 4. Pipe stdin When stdout isn't a TTY, the CLI forwards stdin to the command automatically. ```bash theme={null} echo -e "a\nb\nc" | mirage execute --workspace_id demo --command "wc -l" ``` ### 5. Dry-run with `provision` `provision` returns a `ProvisionResult` (network bytes, cache hits, estimated cost) without running the command -- handy for predicting spend before kicking off an expensive read. ```bash theme={null} mirage provision --workspace_id demo \ --command "cat /s3/data/example.jsonl | wc -l" ``` Every factory-built backend estimates commands out of the box, by family: whole-file readers (`cat`, `sort`, `md5`, ...) charge the byte total from `stat`, `head`/`tail`/`file` charge a bounded range, `grep`/`rg` charge a worst-case full read, and metadata commands (`ls`, `find`, `stat`, `du`, ...) charge op counts only. Transforms (`gzip`, `tar`, `split`, ...) keep the read total as a floor with `precision=unknown` output; `cp` brackets both read and write between 0 (server-side copy) and the source total; metadata writes (`rm`, `mkdir`, `touch`, ...) are zero-byte op counts, with recursive `rm` degrading to a floor; pure commands (`seq`, `date`, `bc`, `expr`) and shell builtins (`echo`, `cd`, ...) are zero-cost. Anything the planner cannot estimate honestly -- `mv` (free rename or full cross-mount copy), `tee` (stdin size), arbitrary programs -- reports `precision=unknown` with all totals as floors, never an error. Virtual files whose size cannot be resolved (for example a rendered `chat.jsonl`) degrade the estimate to `precision=unknown` while keeping the known byte total as a floor. Provision is optional when you register your own commands: leave it out and the planner reports `precision=unknown`. To opt in with one line, reuse the estimator helpers (`make_file_read_provision`, `make_search_provision`, `metadata_provision`, ... in Python; `makeFileReadProvision` and friends in TypeScript), or pass `provision_overrides={"grep": my_estimator}` to the command factory. An explicit `None`/`null` override disables a default. Pipelines combine field-wise: `|`, `;` and `&&` sum the estimates, `||` brackets the branches (cheapest low, priciest high), and `for` loops multiply by the iteration count. A stage downstream of an unknown stage is also unknown, and totals under `precision=unknown` are floors. ### 6. Cache: network → hit after a real read After a real `cat`, `provision` flips that path from a network read to a cache hit (`cache_hits=1`). ```bash theme={null} mirage execute --workspace_id demo --command "cat /s3/data/example.jsonl > /dev/null" mirage provision --workspace_id demo --command "cat /s3/data/example.jsonl" ``` ### 7. Command history Every executed command is recorded by a hidden recorder (the [Observer](/home/observer)). The `history` builtin shows the calling session's commands (GNU bash semantics, `history -c` clears only that session's view), and `/.bash_history` renders the GNU histfile across all sessions, readable with the ordinary file commands. ```bash theme={null} mirage execute --workspace_id demo --command "history 5" mirage execute --workspace_id demo --command "tail -n 6 /.bash_history" mirage execute --workspace_id demo --command "grep cat /.bash_history" ``` ### 8. Background jobs Long-running commands take `--background` and return a `job_id` immediately. `mirage job wait` blocks until it's done. ```bash theme={null} JOB=$(mirage execute --workspace_id demo --background \ --command "wc -l /s3/data/example.jsonl" \ | jq -r .job_id) mirage job wait $JOB ``` ### 9. Snapshot to disk ```bash theme={null} mirage workspace snapshot demo /tmp/demo.tar ``` ### 10. Restore from snapshot Snapshots redact cloud creds at save time, so loading needs fresh creds via a config file. The same workspace YAML used for create works. ```bash theme={null} mirage workspace load /tmp/demo.tar workspace.yaml \ --id demo_loaded mirage workspace get demo_loaded --verbose mirage execute --workspace_id demo_loaded \ --command "head -n 1 /s3/data/example.jsonl" ``` A config file is required for any mount whose snapshot config contains redacted secrets (S3 with inline keys, GDrive, Slack, Discord, Redis, ...). The snapshot stores `""` in place of those secrets at save time. Loading without the config 400s with the list of prefixes that need fresh creds. Local resources (RAM, Disk) restore as-is with no extra config needed. ### 11. Clean up The daemon exits \~30s after the last workspace is deleted. ```bash theme={null} mirage workspace delete demo mirage workspace delete demo_loaded ``` ## Versioning Every workspace has its own git-backed history kept by the daemon (under `~/.mirage/repos/` by default; set `MIRAGE_HOME` to relocate the whole data tree). You commit the live state as a version, then log, diff, branch, and restore in place. The verbs follow git. ### Commit and log ```bash theme={null} mirage execute --workspace_id demo --command "echo v1 > /notes.txt" mirage workspace commit demo -m "first" mirage execute --workspace_id demo --command "echo v2 > /notes.txt" mirage workspace commit demo -m "second" mirage workspace log demo # newest first: "second", then "first" ``` ### Diff `diff` reports changed paths (added / modified / deleted), git-style. No refs compares live state to the branch HEAD; one ref compares live to that ref; two refs compare two versions. ```bash theme={null} mirage workspace diff demo # live vs HEAD mirage workspace diff demo # live vs mirage workspace diff demo # vs ``` ### Branch `branch` forks at another branch's current version; commit onto it with `-b`. ```bash theme={null} mirage workspace branch demo exp # fork exp from main mirage workspace branch demo exp2 --from exp # fork from a non-main branch mirage workspace commit demo -b exp -m "on exp" # commit onto exp mirage workspace log demo -b exp # log a specific branch ``` ### Checkout `checkout` restores the live state to a past version or branch, in place. It overwrites uncommitted live state. ```bash theme={null} mirage workspace checkout demo # by version id mirage workspace checkout demo main # by branch name ``` ### Clone from a version `clone --at` makes a new workspace from one of the source's past versions (omit `--at` to clone the live state). ```bash theme={null} mirage workspace clone demo --at --id demo_at_v1 ``` ## Verbs at a glance | Verb | What it does | | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `mirage workspace create FILE [--id NAME]` | Build resources from YAML, register a workspace, return its id. | | `mirage workspace list` | Brief one-line summary per active workspace. | | `mirage workspace get ID [--verbose]` | Full detail (mounts, sessions, optionally cache / dirty / history internals). | | `mirage workspace delete ID` | Stop the workspace; daemon may exit on the idle timer. | | `mirage workspace clone SRC_ID [--id NAME] [--at REF]` | New workspace from the source; `--at REF` clones a past version, otherwise the live state. | | `mirage workspace commit ID [-m MSG] [-b BRANCH]` | Commit the live state as a version; returns the version id. | | `mirage workspace log ID [-b BRANCH]` | List versions on a branch, newest first. | | `mirage workspace diff ID [A] [B] [-b BRANCH]` | Changed paths (added/modified/deleted). No refs: live vs HEAD; one ref: live vs A; two: A vs B. | | `mirage workspace branch ID NAME [--from BRANCH]` | Fork a branch at another branch's current version. | | `mirage workspace checkout ID REF` | Restore the live state in place to a version id or branch. | | `mirage workspace snapshot ID PATH.tar` | Snapshot to a tar file. | | `mirage workspace load PATH.tar [CONFIG] [--id NAME]` | Restore from tar; optional config re-supplies redacted creds. | | `mirage session create WS [--id NAME] [-m /prefix[:mode]]...` | Add a named session (own cwd + env), optionally restricted to mounts with a mode ceiling (`read`/`write`/`exec` or `r`/`rw`/`rwx`). | | `mirage session list WS` | List sessions for a workspace. | | `mirage session delete WS SESSION` | Close a session. | | `mirage execute --workspace_id WS [--session_id S] [--background] --command "..."` | Run a command. Pipes stdin automatically when stdout is not a TTY. | | `mirage provision --workspace_id WS [--session_id S] --command "..."` | Dry-run / cost estimate -- returns a `ProvisionResult` shape (network bytes, cache hits, estimated cost) without running the command. | | `mirage job list [--workspace_id WS]` | List jobs the daemon has run, plus their status. | | `mirage job get JOB` | Detail for one job. | | `mirage job wait JOB [--timeout SECS]` | Block until the job is done; returns the result. | | `mirage job cancel JOB` | Cancel a running job. | ## Per-mount command limits Cap what a command may stream back per mount with `command_limits`, so a runaway `cat`/`grep`/`rg` can't flood the agent or hang. Each entry sets `max_lines` / `max_bytes` (output cap) and/or `timeout_seconds` (deadline), with `on_exceed: truncate` (stop, exit 0, add a stderr notice) or `on_exceed: error` (stop, exit 1): ```yaml theme={null} mounts: /data: resource: ram mode: WRITE command_limits: head: # cap output, keep going max_lines: 100 on_exceed: truncate grep: # cap output, fail hard max_lines: 50 on_exceed: error rg: # deadline in seconds timeout_seconds: 30 ``` Caps fire on the **terminal** command of a pipeline only, so `cat big.txt | head -n 30` still shows 30 lines. Truncation exits `0`, `error` exits `1`, and a timeout exits `124` -- each with a stderr notice. Without a `command_limits` block, `cat`/`grep`/`rg`/`head`/`tail` still cap at 2000 lines by default. See [Output Limits](/python/quickstart#output-limits) for the SDK form and the same fields. ## Daemon control Most users never run these directly -- the daemon auto-spawns on first `workspace create` and auto-exits after the idle timer fires (default 30s after the last workspace is deleted). When you need to intervene -- typically during development, when you want code changes to take effect: ```bash theme={null} mirage daemon status # health, PID, uptime, workspace count mirage daemon stop # graceful: trip exit event, falls back to SIGTERM after --timeout (default 5s) mirage daemon restart # stop + lazy respawn (add --eager to spawn now) mirage daemon kill # SIGKILL via PID file -- last resort ``` | Verb | What it does | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `mirage daemon status` | Daemon health, PID, uptime, workspace count. Exit 1 if daemon not reachable. | | `mirage daemon stop` | `POST /v1/shutdown` to trip the daemon's exit event. Daemon closes active workspaces and exits. Falls back to SIGTERM on `--timeout`. | | `mirage daemon restart` | Stop, then either wait for next `workspace create` to auto-spawn (default) or `--eager` to spawn immediately. Workspaces are LOST on restart -- save any you want to keep first with `mirage workspace snapshot ` and reload with `mirage workspace load `. | | `mirage daemon kill` | SIGKILL via PID file at `~/.mirage/daemon.pid`. Skips graceful shutdown -- use only when `stop` hangs. | When you change Mirage's source code (commands, providers, etc.), the running daemon won't see your changes -- it loaded the old code at startup. Run `mirage daemon restart` to pick up new code. Same situation as `dockerd` after recompiling Docker. ## Where the daemon lives Most users never need to think about the daemon. If you do: * It listens on `http://127.0.0.1:8765` by default. * Override via env vars or `~/.mirage/config.toml`, edited with `mirage config` (the `git config` of Mirage): ```bash theme={null} mirage config set port 9100 mirage config get port # one key, exit 1 if unset mirage config list # everything written in the file mirage config unset port # remove a key mirage config list --resolved # effective values + where each came from ``` ```toml theme={null} [daemon] url = "http://127.0.0.1:8765" idle_grace_seconds = 30 port = 9100 ``` * Per key the precedence is: env var > `config.toml` > default. Because env vars silently beat the file, `mirage config list` (which shows only the file) can look right while the daemon uses something else. `--resolved` shows the value each key will actually get and which source won: ```bash theme={null} $ mirage config set port 9100 $ export MIRAGE_DAEMON_PORT=9200 # e.g. left over in your shell profile $ mirage config list # the file looks correct... port = 9100 $ mirage config list --resolved # ...but the env var wins port = 9200 (env MIRAGE_DAEMON_PORT) url = http://127.0.0.1:8765 (default) auth_token = *** (env MIRAGE_TOKEN) ``` Use it whenever a setting seems ignored: the origin column names the exact env var overriding you. Secrets are masked, and piped output is JSON (`{"port": {"value": ..., "origin": ...}}`) so it works with `jq`. * Allowed keys: `url`, `socket`, `auth_token`, `auth_mode`, `allowed_hosts`, `idle_grace_seconds`, `port`, and the `jwt_*` family (`jwt_alg`, `jwt_issuer`, `jwt_audience`, `jwt_pubkey_file`, `jwt_clock_skew`, `jwt_authorized_parties`). `MIRAGE_HOME` and raw secrets (`MIRAGE_AUTH_TOKEN`, `MIRAGE_JWT_PUBKEY`) stay env-only. Data locations are not keys: like docker's `data-root` and git's `GIT_DIR`, `MIRAGE_HOME` is the single configurable root and its layout (`daemon.pid`, `repos/`, `snapshots/`, `state/`) is fixed. * Like `dockerd` with a bad `daemon.json`, the daemon refuses to start on unknown keys or malformed TOML, naming the offender. `mirage config unset ` accepts unknown keys so you can repair the file; `mirage config list` warns about them. * Settings take effect on the next daemon start; there is no hot reload. * `mirage config set` chmods the file to `0600` since it may hold `auth_token`. * Logs go to `~/.mirage/daemon.log` when the CLI auto-spawns it. * It exits 30 seconds after the workspace count hits zero (configurable). # Installation Source: https://docs.mirage.strukto.ai/home/install Install Mirage with the Python package mirage-ai or the TypeScript packages @struktoai/mirage-node and @struktoai/mirage-browser. ## Prerequisites * **Python** ≥ 3.11 for the `mirage-ai` package and the `mirage` CLI * **Node.js** ≥ 20 for the TypeScript SDK * **macOS** or **Linux** (FUSE-based mounts require platform support) ## Python ```bash theme={null} uv add mirage-ai ``` This installs both the `mirage` library and the `mirage` CLI binary. ## TypeScript Pick the package that matches your runtime — `@struktoai/mirage-core` is auto-pulled by both Node and browser entrypoints. ```bash theme={null} npm install @struktoai/mirage-node # Node.js servers and CLIs npm install @struktoai/mirage-browser # browser / edge runtimes npm install @struktoai/mirage-agents # OpenAI / Vercel AI / LangChain / Mastra adapters ``` ## CLI ```bash theme={null} curl -fsSL https://strukto.ai/mirage/install.sh | sh # or npm install -g @struktoai/mirage-cli # or uvx mirage-ai # or npx @struktoai/mirage-cli ``` ## More Details Resource extras, virtualenv setup, and `uv` workflow. Native peers (FUSE, Redis) and per-runtime notes for Node, browser, and edge. # Introduction Source: https://docs.mirage.strukto.ai/home/introduction Unified Virtual Filesystem for AI Agents.
Mirage Mirage

Mirage in 30 Seconds

Mount your resources, then run shell commands across them. The same echo, ls, grep work against an in-memory mount, S3, Slack, etc.

```python theme={null} import os from mirage import Mount, MountMode, Workspace from mirage.resource.ram import RAMResource from mirage.resource.s3 import S3Config, S3Resource from mirage.resource.slack import SlackConfig, SlackResource ws = Workspace({ "/data": Mount(RAMResource(), mode=MountMode.WRITE), "/s3": S3Resource(S3Config(bucket="my-bucket")), "/slack": SlackResource(SlackConfig(token=os.environ["SLACK_BOT_TOKEN"])), }) # Same shell vocabulary, three different backends. await ws.execute('echo "hello mirage" > /data/hello.txt') await ws.execute("ls /s3/reports/") result = await ws.execute('grep -r "release" /slack/channels/eng__C04QX') print(await result.stdout_str()) ``` ```typescript theme={null} import { Mount, MountMode, RAMResource, S3Resource, SlackResource, Workspace, } from '@struktoai/mirage-node' const ws = new Workspace({ '/data': new Mount(new RAMResource(), { mode: MountMode.WRITE }), '/s3': new S3Resource({ bucket: 'my-bucket' }), '/slack': new SlackResource({ token: process.env.SLACK_BOT_TOKEN! }), }) // Same shell vocabulary, three different backends. await ws.execute('echo "hello mirage" > /data/hello.txt') await ws.execute('ls /s3/reports/') const res = await ws.execute('grep -r "release" /slack/channels/eng__C04QX') console.log(new TextDecoder().decode(res.stdout)) ``` ```bash theme={null} cat > workspace.yaml <<'EOF' mode: WRITE mounts: /data: resource: ram mode: WRITE /s3: resource: s3 mode: READ config: bucket: ${AWS_S3_BUCKET} /slack: resource: slack mode: READ config: token: ${SLACK_BOT_TOKEN} EOF mirage workspace create workspace.yaml --id demo mirage execute -w demo -c 'echo "hello mirage" > /data/hello.txt' mirage execute -w demo -c 'ls /s3/reports/' mirage execute -w demo -c 'grep -r "release" /slack/channels/eng__C04QX' ```

Add GitHub, Postgres, SSH, Notion, Google Drive, ... and the same shell vocabulary keeps working. That's the whole pitch.

What is Mirage?

MirageMirageMirage is a Unified Virtual Filesystem for AI agents. It mounts your apps, services, and systems behind one filesystem interface, so an agent reaches every backend with the same handful of Unix-like tools instead of a new SDK per service.

One Filesystem

Every service speaks the same filesystem semantics, so agents reason about one abstraction instead of N SDKs and M MCPs. S3, R2, Google Drive, GitHub, Linear, Notion, Slack, Discord, MongoDB, Redis, SSH, and more mount side-by-side under a single root.

Familiar Bash Tools

Agents reuse the same handful of Unix-like tools (ls, find, grep, cat, ...) instead of learning a new API per service. Pipelines compose across services as naturally as on a local disk, the exact corpus modern LLMs are most heavily trained on.

```bash theme={null} # Find every mention of "mirage" across three services grep -r "mirage" /slack /gmail /github ```

Portable, Versioned Workspaces

Snapshot, clone, and version a workspace the way git treats source. Move agent runs between machines without restarting, fork from any past state, and replay a run on demand.

Embed in Apps and Agents

Python and TypeScript SDKs give your AI agents a virtual filesystem directly inside FastAPI, Express, browser apps, or any async runtime, no separate process required. Works with the major agent frameworks (OpenAI Agents SDK, Vercel AI SDK, LangChain, Pydantic AI, CAMEL, OpenHands) and a lightweight CLI plugs into coding agents like Claude Code and Codex.

A Real-world Example

An agent watches your team's Slack #incident channel. A user posts a screenshot of mirage --help with the message "the CLI design is confusing and hard to follow".

Built with the OpenAI Agents SDK, the agent walks Slack, GitHub, and Linear through one bash tool.

Code

```python theme={null} from agents import Runner from agents.run import RunConfig from agents.sandbox import SandboxAgent, SandboxRunConfig from mirage import Mount, MountMode, Workspace from mirage.agents.openai_agents import MirageSandboxClient from mirage.resource.github import GitHubConfig, GitHubResource from mirage.resource.linear import LinearConfig, LinearResource from mirage.resource.slack import SlackConfig, SlackResource slack = SlackResource(SlackConfig(token="xoxb-...")) github = GitHubResource( GitHubConfig(token="github_pat_..."), owner="strukto-ai", repo="mirage", ) linear = LinearResource(LinearConfig(api_key="lin_api_...")) ws = Workspace({ "/slack": slack, "/github": github, "/linear": Mount(linear, mode=MountMode.WRITE), }) agent = SandboxAgent( name="Design feedback triage", model="gpt-5.5", instructions=ws.file_prompt, ) config = RunConfig(sandbox=SandboxRunConfig(client=MirageSandboxClient(ws))) task = ( "Scan recent messages in the Slack #incident channel. If anyone posts " "feedback about Mirage with a screenshot, read the image, locate the " "relevant CLI code in the Mirage GitHub repo, and file a design issue " "in Linear with the screenshot, the user's feedback, and links to the " "offending source files." ) result = await Runner.run(agent, task, run_config=config) print(result.final_output) ``` ```typescript theme={null} import { GitHubResource, LinearResource, Mount, MountMode, SlackResource, Workspace, } from '@struktoai/mirage-node' import { Agent, run, shellTool } from '@openai/agents' import { MirageShell, buildSystemPrompt } from '@struktoai/mirage-agents/openai' const slack = new SlackResource({ token: process.env.SLACK_BOT_TOKEN! }) const github = await GitHubResource.create({ token: process.env.GITHUB_TOKEN!, owner: 'strukto-ai', repo: 'mirage', }) const linear = new LinearResource({ apiKey: process.env.LINEAR_API_KEY! }) const ws = new Workspace({ '/slack': slack, '/github': github, '/linear': new Mount(linear, { mode: MountMode.WRITE }), }) const agent = new Agent({ name: 'Design feedback triage', model: 'gpt-5.5', instructions: buildSystemPrompt({ workspace: ws }), tools: [shellTool({ shell: new MirageShell(ws) })], }) const task = 'Scan recent messages in the Slack #incident channel. If anyone posts ' + 'feedback about Mirage with a screenshot, read the image, locate the ' + 'relevant CLI code in the Mirage GitHub repo, and file a design issue ' + 'in Linear with the screenshot, the user\'s feedback, and links to the ' + 'offending source files.' const result = await run(agent, task) console.log(result.finalOutput) ```

Runnable source: examples/typescript/agents/openai/multi\_resource\_agent.ts, the same shell-tool pattern over Slack and S3.

Walk-through

Inside the workspace shell, the agent runs three steps:

```bash theme={null} # 1. Read the latest #incident message + list its attachments $ cat /slack/channels/incident__C0B0DB9K11T/2026-04-28/chat.jsonl $ ls /slack/channels/incident__C0B0DB9K11T/2026-04-28/files/ # image__F0B01A3R171.png <- the agent reads this via the model's vision input # 2. Find the CLI source the screenshot is complaining about $ rg -n "Mirage daemon CLI|workspace|session|provision" /github/typescript $ cat /github/typescript/packages/cli/src/main.ts # 3. File a design issue in Linear with the feedback + code refs $ linear issue create --team_id \ --title "[Design] Rework Mirage CLI top-level command surface" \ --description "$(cat <<'EOF' ... feedback, screenshot summary, and links to the offending files ... EOF )" ```

The agent files a new issue in Linear with the user's feedback and links to the relevant source files.

Use Cases

Mirage shows up wherever an agent needs to read, write, or stitch together data that doesn't already live on a local disk.

Watch Slack, search GitHub, file Linear issues. One shell, no per-service wiring. Point Claude Code or Codex at S3, Postgres, or SSH hosts as if they were files. READ + WRITE mounts on Notion, Drive, and Linear so the agent can edit and comment back. tail, grep, jq over remote logs, metrics, and config without per-source plugins. Embed inside Daytona, E2B, Modal, or Vercel sandboxes as the data plane. Snapshot, restore, and clone workspaces for branchable, replayable agent runs.

FAQ

No. Mirage runs the workspace in-process: ws.execute(...) parses and dispatches commands without mounting anything on the host. FUSE is an optional surface if you also want host tools (editors, language servers, rg) to see the workspace. The shell runs in your Mirage process. It's a tree-sitter bash parser plus a custom executor that routes commands to per-mount handlers, so there is no subshell to /bin/bash and no os.system. Most common Unix verbs work (ls, cat, grep, find, head, wc, jq, ...) plus pipes, redirects, globs, and &&/||. The shell only sees your mounted resources: no arbitrary host filesystem, no shelling out to host binaries. For untrusted code you'd still want a real sandbox (Daytona, E2B, Modal); Mirage embeds inside those rather than replacing them. Backend-bound. cat /s3/... is one GetObject; find /postgres/... is a SQL query. Reads cache per session, and mirage provision returns a dry-run estimate (network bytes, cache hits, projected cost) before you commit to an expensive operation. Self-hosted. Mirage is a library plus a thin local daemon. The daemon lives in your process or sandbox; data only leaves your network if a mount you configured already does (e.g. an S3 read). Anything that exposes a shell tool: Claude Code, Codex / OpenAI Agents SDK, Cursor, OpenHands, Pydantic deepagents. Direct SDK integrations live under Python agents and TypeScript agents. Both. The Python package is the reference implementation; the TypeScript SDK (@struktoai/mirage-node) ships the same Workspace/execute surface and most resources. Some agent integrations land on Python first. Resources are pluggable. The "Add a resource" guide walks through the read/write/stat surface a new backend implements. PRs welcome.

Explore Mirage

Create a workspace, mount resources, and run shell commands in minutes. Same Workspace API in Node, browser, and edge runtimes. Drive workspaces from the shell: create, execute, snapshot, and restore. Compare resources by mount mode, setup path, and common use cases.

Community & Support

Setup failures, credential issues, and FUSE gotchas. Talk to the team directly if you are blocked. Chat with the community and get help from the team. Report bugs, request features, or ask questions.
# Observer Source: https://docs.mirage.strukto.ai/home/observer A hidden recorder that captures every command and file op as timestamped events, backed by a pluggable store. Powers command history. ## What It Does Every workspace has one **Observer**: a hidden recorder that logs each top-level command and its file ops as timestamp-ordered events. It owns no mount and has no endpoint of its own, features like command history are just *views* over its events. Nested evals (`$(...)`, `eval`, `source`, `xargs`) run without recording, so only real top-level commands land. ```mermaid theme={null} flowchart TD Exec["ws.execute(cmd)"] --> Obs[Observer] Obs --> Store[("ObserverStore
RAM · Disk · Redis")] Store --> Hist["history builtin
(calling session)"] Store --> File["/.bash_history
(all sessions)"] ``` ## Storage backends The Observer holds a storage-agnostic `ObserverStore`. RAM is the default; swap it to persist events across daemon restarts. The store is chosen at construction; there is no runtime API to change it. ```python Python theme={null} from mirage import Workspace, MountMode from mirage.resource.ram import RAMResource from mirage.observe.disk_store import DiskObserverStore from mirage.observe.redis_store import RedisObserverStore # RAM (default), nothing to configure ws = Workspace({"/data": RAMResource()}, mode=MountMode.WRITE) # Persist to disk ws = Workspace({"/data": RAMResource()}, mode=MountMode.WRITE, observe=DiskObserverStore("/var/mirage/history")) # Persist to Redis ws = Workspace({"/data": RAMResource()}, mode=MountMode.WRITE, observe=RedisObserverStore("redis://localhost:6379/0")) ``` ```typescript TypeScript theme={null} import { Workspace, RAMResource } from '@struktoai/mirage-core' import { DiskObserverStore, RedisObserverStore } from '@struktoai/mirage-node' // RAM (default), nothing to configure const ws = new Workspace({ '/data': new RAMResource() }) // Persist to disk const wsDisk = new Workspace( { '/data': new RAMResource() }, { observe: new DiskObserverStore('/var/mirage/history') }, ) // Persist to Redis const wsRedis = new Workspace( { '/data': new RAMResource() }, { observe: new RedisObserverStore({ url: 'redis://localhost:6379/0' }) }, ) ``` ## Supported: command history The Observer powers a GNU-bash-compatible history, exposed two ways over the same events: | Surface | Scope | Notes | | ---------------------- | --------------- | ------------------------------------------------------------ | | `history` builtin | calling session | GNU flags `-c -d -a -n -r -w -s -p` and a count arg | | `/.bash_history` mount | all sessions | read-only, GNU histfile format (`#` then the command) | Because `/.bash_history` is a real read-only mount, the ordinary file commands work on it directly: ```bash theme={null} history 5 tail -n 6 /.bash_history grep cat /.bash_history ``` The format is GNU bash (`#`), not zsh (`: :;`). ## Snapshots History is part of the workspace state: the Observer's command, clear, and delete events are captured into a [snapshot](/home/snapshot) and restored on load, so a restored workspace replays with the same history. The `/.bash_history` view mount itself is not stored, it is a live projection rebuilt from the events. # Policy Engine Source: https://docs.mirage.strukto.ai/home/policy-engine Script which runtime serves each command line; policy scripts run on the workspace's evaluator runtime. A workspace can hold several runtimes for the same command, with different trade-offs. `monty` runs `python3` in-process, sandboxed, stdlib-only: perfect for quick one-liners and pipe transforms, useless for a script that imports `pandas`. A `docker` entry is the opposite: your container, your installed packages, but every line pays the exec round-trip. The **policy** decides per line which one serves it: ```yaml theme={null} mode: exec runtimes: - monty # captures python3 by default: sandboxed, stdlib-only - name: docker captures: ["python3"] # your container, has pandas installed config: container: my-box - vfs policy: ./policy.py ``` ```python policy.py theme={null} # Jobs under /jobs need the container's packages; anything else # stays on the fast in-process sandbox. The LAST EXPRESSION is # the verdict. stage = ctx["commands"][0] in_jobs = any(p.startswith("/jobs/") for p in stage["paths"]) "docker" if in_jobs else "monty" ``` So `python3 /jobs/train.py --epochs 3` runs in the container, while `cat data.csv | python3 -c "import sys; print(len(sys.stdin.read()))"` stays on monty. ## Input: the line's context The script sees one global, `ctx`, the parsed line before any routing. This payload is captured from a live run of the config above, routing `python3 /jobs/train.py --epochs 3`: ```json theme={null} { "line": "python3 /jobs/train.py --epochs 3", "commands": [ { "command": "python3", "words": ["python3", "/jobs/train.py", "--epochs", "3"], "builtin": true, "paths": ["/jobs/train.py"] } ], "command": "python3", "builtin": true, "cwd": "/", "env": {}, "session_id": "019fb2fc-19a3-77cf-bbbe-c76069eed523", "agent_id": "", "mounts": ["/.bash_history/", "/data/", "/dev/", "/"] } ``` * `commands`: one entry per pipeline stage (`cat x | python3 -` has two), each with its full words and its absolute-path operands. * `command`, `builtin`: mirror the first stage. `builtin` means mirage has a builtin spec for the command (`python3` is a builtin that hands its code to a runtime), as opposed to an unknown name. * `env`: the session environment (empty here, no `export` yet). * `session_id`: the executing session's id. `agent_id` is empty until an agent identity is attached. * `mounts`: the workspace's mount prefixes, including built-ins like `/dev/`. The payload round-trips through JSON (`PolicyContext.from_dict`), so you can store one and replay a decision in tests. ## Output: the verdict * **A runtime name** (`"docker"`): that entry serves every command it captures on this line. * **`None`**: no opinion; the first capturer in the `runtimes` list order serves each command (here, `monty`). * **`{"deny": reason}`**: the line is refused before anything runs. It exits 126 with `: policy denied: ` on stderr. * Anything else is a routing error; the line fails rather than guessing. `{"runtime": "docker"}` is the dict spelling of a name, and the dict is where the verdict grows: new powers arrive as new keys, never as new return types. In code, the same two arms have a typed spelling, `RouteResult` and `DenyResult`; the dict is their wire form, the only shape a script can return from inside the evaluator sandbox: ```python theme={null} from mirage.runtime.policy import (DenyResult, PolicyContext, PolicyResult, RouteResult) def policy(ctx: PolicyContext) -> PolicyResult | None: if any(p.startswith("/prod/") for p in ctx.commands[0].paths): return DenyResult("writes under /prod are blocked") return RouteResult("docker") if "/jobs/" in ctx.line else None ``` Besides the global `policy`, each runtime entry can carry its own `script:` with the same `ctx` input but a boolean verdict: am I willing to serve this line? Unwilling entries step aside and the first willing capturer wins. ## What runs the scripts `policy.py` is never imported: the workspace evaluates its source on the **policy engine**, an entry in the `runtimes` list with the evaluator capability. The script's file extension picks the engine: `policy: ./policy.py` runs on the first python evaluator (monty, or pyodide in TypeScript) and `policy: ./policy.js` on the first JS evaluator (quickjs), even when an evaluator of the other language sits earlier in the list; with no language match the first evaluator serves. A config with policy scripts but no evaluator entry fails at the first decision. Evaluation is bounded: a policy script that hangs fails the line with a policy error after 10 seconds instead of freezing the workspace. When you build the workspace in code rather than from a config file, `policy=` also accepts a plain function with the same decision contract, sync or async. It is called directly, so no evaluator is involved: ```python theme={null} from mirage.runtime.policy import PolicyContext def policy(ctx: PolicyContext) -> str | None: stage = ctx.commands[0] in_jobs = any(p.startswith("/jobs/") for p in stage.paths) return "docker" if in_jobs else "monty" ws = Workspace(mounts, runtimes=runtimes, policy=policy) ``` A per-entry `script=` likewise accepts `(ctx) -> bool` in code. ## Bring your own The capability is open: inherit `EvaluatorMixin` (Python) or implement `Evaluator` with the `EVALUATOR` brand (TypeScript) on your own runtime, and it becomes eligible as the policy engine. `eval(code, inputs=...)` returns the last expression's value; how the value travels is your runtime's choice. The docker examples give a stock container the capability by piping a harness to `python3 -`: [python](https://github.com/strukto-ai/mirage/blob/main/examples/python/runtimes/docker/docker_eval.py), [typescript](https://github.com/strukto-ai/mirage/blob/main/examples/typescript/runtimes/docker/docker_eval.ts). # Resource Matrix Source: https://docs.mirage.strukto.ai/home/resource-matrix Compare Mirage resources by access, runtime, and setup path. ## How To Read This Matrix Use this page to pick the first resource to try. If you want the fastest path, start with RAM or disk. If you need real integrations, jump from the setup guide to the resource docs. The **Access** column separates filesystem access from API actions. `read`, `write`, and `exec` are `MountMode` values; `exec` allows commands to launch binaries from the mount and is typically used with Disk or RAM. `actions` means the resource also exposes mutating commands such as sending a message or creating an issue; run those commands on a `WRITE` mount. The **Docs** column shows where each resource is available, with one icon per supported runtime: Python, TypeScript (Node), and TypeScript (Browser). Click an icon to jump to that runtime's docs. Sections below mirror the **Setup** sidebar so you can pick a category and follow the same path through credentials → resource docs. ## Infrastructure No external setup, these run locally or against a connection string you already have. | Resource | Access | Docs | Notes | | -------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------- | | RAM | read, write, exec | [](/python/resource/ram) [](/typescript/quickstart) [](/typescript/quickstart) | Best first-run option | | Disk | read, write, exec | [](/python/resource/disk) [](/typescript/setup/disk) | Local filesystem bridge | | OPFS | read, write, exec | [](/typescript/setup/opfs) | Browser-only persistent filesystem | | Redis | read, write, exec | [](/python/resource/redis) [](/typescript/setup/redis) | Persistent cache-backed workspace | | SSH | read, write | [](/python/resource/ssh) [](/typescript/setup/ssh) | Remote filesystem access | ## Object Storage | Resource | Access | Setup | Docs | Notes | | ------------------- | ----------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | | S3 | read, write | [S3](/home/setup/s3) | [](/python/resource/s3) [](/typescript/setup/s3) [](/typescript/setup/s3) | Common cloud object store | | R2 | read, write | [R2](/home/setup/r2) | [](/python/resource/r2) [](/typescript/setup/r2) [](/typescript/setup/r2) | S3-style API | | GCS | read, write | [GCS](/home/setup/gcs) | [](/python/resource/gcs) [](/typescript/setup/gcs) [](/typescript/setup/gcs) | GCP object storage | | OCI | read, write | [OCI](/home/setup/oci) | [](/python/resource/oci) [](/typescript/setup/oci) [](/typescript/setup/oci) | Oracle object storage | | Supabase | read, write | [Supabase](/home/setup/supabase) | [](/python/resource/supabase) [](/typescript/setup/supabase) [](/typescript/setup/supabase) | Storage-oriented workflows | | MinIO | read, write | [MinIO](/home/setup/minio) | [](/python/resource/minio) [](/typescript/setup/minio) [](/typescript/setup/minio) | Self-hosted S3-compatible store | | Ceph | read, write | [Ceph](/home/setup/ceph) | [](/python/resource/ceph) [](/typescript/setup/ceph) [](/typescript/setup/ceph) | Ceph Rados Gateway (self-hosted) | | SeaweedFS | read, write | [SeaweedFS](/home/setup/seaweedfs) | [](/python/resource/seaweedfs) [](/typescript/setup/seaweedfs) [](/typescript/setup/seaweedfs) | SeaweedFS S3 gateway (self-hosted; backs Blaxel Agent Drive) | | Wasabi | read, write | [Wasabi](/home/setup/wasabi) | [](/python/resource/wasabi) [](/typescript/setup/wasabi) [](/typescript/setup/wasabi) | Low-cost S3-compatible cloud | | Backblaze B2 | read, write | [Backblaze B2](/home/setup/backblaze) | [](/python/resource/backblaze) [](/typescript/setup/backblaze) [](/typescript/setup/backblaze) | B2 via S3 API | | DigitalOcean Spaces | read, write | [DigitalOcean](/home/setup/digitalocean) | [](/python/resource/digitalocean) [](/typescript/setup/digitalocean) [](/typescript/setup/digitalocean) | DO Spaces via S3 API | | Tencent COS | read, write | [Tencent COS](/home/setup/tencent) | [](/python/resource/tencent) [](/typescript/setup/tencent) [](/typescript/setup/tencent) | Tencent Cloud Object Storage | | Alibaba OSS | read, write | [Alibaba OSS](/home/setup/aliyun) | [](/python/resource/aliyun) [](/typescript/setup/aliyun) [](/typescript/setup/aliyun) | Aliyun Object Storage Service | | Scaleway | read, write | [Scaleway](/home/setup/scaleway) | [](/python/resource/scaleway) [](/typescript/setup/scaleway) [](/typescript/setup/scaleway) | Scaleway Object Storage | | QingStor | read, write | [QingStor](/home/setup/qingstor) | [](/python/resource/qingstor) [](/typescript/setup/qingstor) [](/typescript/setup/qingstor) | QingCloud Object Storage | | HF Buckets | read, write | [HF Buckets](/home/setup/hf_buckets) | [](/python/resource/hf_buckets) [](/typescript/setup/hf_buckets) | Hugging Face Buckets | | HF Datasets | read, write | [HF Datasets](/home/setup/hf_datasets) | [](/python/resource/hf_datasets) [](/typescript/setup/hf_datasets) | Hugging Face Dataset repos (lazy reads) | | HF Models | read, write | [HF Models](/home/setup/hf_models) | [](/python/resource/hf_models) [](/typescript/setup/hf_models) | Hugging Face Model repos (weights stream on demand) | | HF Spaces | read, write | [HF Spaces](/home/setup/hf_spaces) | [](/python/resource/hf_spaces) [](/typescript/setup/hf_spaces) | Hugging Face Space repos (app code, configs) | ## Google Workspace | Resource | Access | Setup | Docs | Notes | | -------- | -------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | | Gmail | read, actions | [Google](/home/setup/google) | [](/python/resource/gmail) [](/typescript/setup/gmail) [](/typescript/setup/gmail) | Read, send, reply, forward, and triage mail | | Drive | read, write, actions | [Google](/home/setup/google) | [](/python/resource/gdrive) [](/typescript/setup/gdrive) [](/typescript/setup/gdrive) | Read-write file tree plus the `gws` Google API commands | | Docs | read, actions | [Google](/home/setup/google) | [](/python/resource/gdocs) [](/typescript/setup/gdocs) [](/typescript/setup/gdocs) | Read plus document create/update commands | | Sheets | read, actions | [Google](/home/setup/google) | [](/python/resource/gsheets) [](/typescript/setup/gsheets) [](/typescript/setup/gsheets) | Read plus spreadsheet create/update commands | | Slides | read, actions | [Google](/home/setup/google) | [](/python/resource/gslides) [](/typescript/setup/gslides) [](/typescript/setup/gslides) | Read plus presentation create/update commands | ## Microsoft | Resource | Access | Setup | Docs | Notes | | ---------- | ----------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | OneDrive | read, write | [OneDrive](/home/setup/onedrive) | [](/python/resource/onedrive) [](/typescript/setup/onedrive) [](/typescript/setup/onedrive) | Files and folders through Microsoft Graph | | SharePoint | read, write | [SharePoint](/home/setup/sharepoint) | [](/python/resource/sharepoint) [](/typescript/setup/sharepoint) [](/typescript/setup/sharepoint) | Document libraries through Microsoft Graph; `site`, `drive`, and `key_prefix` can scope a mount to one folder subtree | ## Cloud Files | Resource | Access | Setup | Docs | Notes | | ----------------- | ----------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | Databricks Volume | read, write | [Databricks Volume](/home/setup/databricks) | [](/python/resource/databricks_volume) [](/typescript/setup/databricks_volume) | Unity Catalog volume subtree; mv/cp are non-atomic copies | | Dropbox | read, write | [Dropbox](/home/setup/dropbox) | [](/python/resource/dropbox) [](/typescript/dropbox) [](/typescript/dropbox) | OAuth2 + PKCE; Python, Node, and browser; `root_path` mounts a subfolder; `content_search` narrows grep/rg via the search API | | Box | read, write | [Box](/home/setup/box) | [](/python/resource/box) [](/typescript/box) [](/typescript/box) | OAuth2 + PKCE or developer token; Python, Node, and browser; `root_folder_id` mounts a subfolder; all items served as raw bytes | | Nextcloud | read, write | [Nextcloud](/home/setup/nextcloud) | [](/python/resource/nextcloud) | Self-hosted Nextcloud / ownCloud / WebDAV; HTTP Basic auth with app password | ## Code & DevOps | Resource | Access | Setup | Docs | Notes | | -------- | ------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | | GitHub | read | [GitHub](/home/setup/github) | [](/python/resource/github) [](/typescript/setup/github) [](/typescript/setup/github) | Repository browsing | | Linear | read, actions | [Linear](/home/setup/linear) | [](/python/resource/linear) [](/typescript/setup/linear) [](/typescript/setup/linear) | Read, create, update, comment on, and organize issues | | Langfuse | read | [Langfuse](/home/setup/langfuse) | [](/python/resource/langfuse) [](/typescript/setup/langfuse) [](/typescript/setup/langfuse) | Trace exploration | ## Messaging | Resource | Access | Setup | Docs | Notes | | -------- | ------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | Slack | read, actions | [Slack](/home/setup/slack) | [](/python/resource/slack) [](/typescript/slack) [](/typescript/slack) | Read, post, reply, react, and search | | Discord | read, actions | [Discord](/home/setup/discord) | [](/python/resource/discord) [](/typescript/discord) [](/typescript/discord) | Read history, send messages, and add reactions | | Email | read, actions | [Email](/home/setup/email) | [](/python/resource/email) [](/typescript/setup/email) | Read, send, reply, forward, and triage; browser blocked by raw TCP | ## Database | Resource | Access | Setup | Docs | Notes | | -------- | ----------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | | MongoDB | read | [MongoDB](/home/setup/mongodb) | [](/python/resource/mongodb) [](/typescript/setup/mongodb) [](/typescript/setup/mongodb) | Collection-backed views | | GridFS | read, write | [GridFS](/home/setup/gridfs) | [](/python/resource/gridfs) [](/typescript/setup/gridfs) | File storage on MongoDB with revisions; find runs server-side | | Postgres | read | [Postgres](/home/setup/postgres) | [](/python/resource/postgres) [](/typescript/setup/postgres) [](/typescript/setup/postgres) | SQL tables exposed as paths | | LanceDB | read | [LanceDB](/home/setup/lancedb) | [](/python/resource/lancedb) [](/typescript/setup/lancedb) | Label folders + semantic search command | | Qdrant | read | [Qdrant](/home/setup/qdrant) | [](/python/resource/qdrant) [](/typescript/setup/qdrant) [](/typescript/setup/qdrant) | Collections as folders + semantic search command | ## Knowledge | Resource | Access | Setup | Docs | Notes | | -------- | ------ | ---------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | Dify | read | [Dify](/home/setup/dify) | [](/python/resource/dify) | Knowledge documents and retrieval search | | Chroma | read | [Chroma](/home/setup/chroma) | [](/python/resource/chroma) [](/typescript/setup/chroma) | ChromaDB collection exposed as files and vector search | ## Memory | Resource | Access | Setup | Docs | Notes | | -------- | ------------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | | Mem0 | read, search | [Mem0](/home/setup/mem0) | [](/python/resource/mem0) [](/typescript/setup/mem0) [](/typescript/setup/mem0) | Scoped memories as JSON files plus semantic search | ## Notes | Resource | Access | Setup | Docs | Notes | | -------- | ------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | | Notion | read, actions | [Notion](/home/setup/notion) | [](/python/resource/notion) [](/typescript/setup/notion) [](/typescript/setup/notion) | Read pages; create pages, append blocks, and add comments | ## Others | Resource | Access | Setup | Docs | Notes | | -------- | ------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | | Trello | read, actions | [Trello](/home/setup/trello) | [](/python/resource/trello) [](/typescript/setup/trello) [](/typescript/setup/trello) | Read, create, update, move, label, and comment on cards | ## Agent Frameworks Mirage drops into the major agent application frameworks. Each adapter exposes a `Workspace` as the agent's filesystem and shell. Click an icon to jump to the integration docs. | Framework | Docs | Notes | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | OpenAI Agents SDK | [](/python/agents/openai-agents) [](/typescript/agents/openai) [](/typescript/agents/openai) | Shell, editor, and multimodal file-reading tools for Mirage workspaces. | | Vercel AI SDK | [](/typescript/agents/vercel) [](/typescript/agents/vercel) | `mirageTools()` returns five typed tools for `generateText` / `streamText`. | | LangChain (Deep Agents) | [](/python/agents/langchain) [](/typescript/agents/langchain) | `LangchainWorkspace` backend for Python and Node Deep Agents. | | Pi Coding Agent | [](/typescript/agents/pi) | Mirage extension for `@earendil-works/pi-coding-agent`. Node only. | | OpenCode | [](/typescript/agents/opencode) | Native installable plugin with Mirage tools and per-session stale-write protection. Node only. | | Mastra | [](/typescript/agents/mastra) | `mirageTools()` for Mastra `Agent` definitions. Node only. | | Pydantic AI | [](/python/agents/pydantic-ai) | For pydantic-ai and pydantic-deepagents. | | CAMEL-AI | [](/python/agents/camel) | For CAMEL `ChatAgent`. | | OpenHands | [](/python/agents/openhands) | For the OpenHands agent SDK. | | Claude Code (CLI) | [](/python/agents/claude-code) [](/typescript/agents/claude-code) | Mount via FUSE; run `claude` against the mountpoint. | | Codex (CLI and app) | [](/python/agents/codex) [](/typescript/agents/codex) | Python uses FUSE; TypeScript provides the plugin with Mirage tools and stale-write protection, plus FUSE. | | Grok Build | [](/python/agents/grok-build) [](/typescript/agents/grok-build) | Python uses FUSE; TypeScript provides the Grok plugin with Mirage tools, plus FUSE. | ## Recommended Starting Points * Use [RAM](/python/resource/ram) if you want to learn Mirage itself. * Use [Disk](/python/resource/disk) if you want local file interoperability. * Use [S3](/python/resource/s3) or [GitHub](/python/resource/github) if you want a realistic first external integration. # Alibaba OSS Source: https://docs.mirage.strukto.ai/home/setup/aliyun Set up Alibaba Cloud OSS credentials for the Aliyun resource. ## Credentials ### 1. Create an AccessKey pair 1. Sign in to the [Alibaba Cloud console](https://www.aliyun.com/) 2. Go to **AccessKey Management** (RAM user recommended) -> **Create AccessKey** 3. Copy the **AccessKey ID** and **AccessKey Secret** 4. Note your bucket's **region** (e.g. `cn-hangzhou`) and **bucket** name The endpoint is derived from `region` as `s3.oss-.aliyuncs.com`. Note the `s3.` prefix: this is the S3-compatible host, distinct from the native `oss-.aliyuncs.com`. ### 2. Set environment variables ```bash theme={null} # .env.development OSS_BUCKET=my-bucket OSS_REGION=cn-hangzhou OSS_ACCESS_KEY_ID=... OSS_ACCESS_KEY_SECRET=... ``` For Python configuration, see the [Alibaba OSS resource](/python/resource/aliyun) docs. # Backblaze B2 Source: https://docs.mirage.strukto.ai/home/setup/backblaze Set up Backblaze B2 credentials for the Backblaze resource. ## Credentials ### 1. Create an application key 1. Sign in to [Backblaze](https://secure.backblaze.com/) -> **B2 Cloud Storage** 2. Go to **Application Keys** -> **Add a New Application Key** 3. Scope it to your bucket with at least **Read** access 4. Copy the **keyID** (access key id) and **applicationKey** (secret access key) 5. Note your bucket's **endpoint region** from the bucket details (e.g. `us-west-004`) and the **bucket** name The endpoint is derived from `region` as `s3..backblazeb2.com`. ### 2. Set environment variables ```bash theme={null} # .env.development B2_BUCKET=my-bucket B2_REGION=us-west-004 B2_ACCESS_KEY_ID=... # B2 keyID B2_SECRET_ACCESS_KEY=... # B2 applicationKey ``` For Python configuration, see the [Backblaze resource](/python/resource/backblaze) docs. # Box Source: https://docs.mirage.strukto.ai/home/setup/box Set up Box OAuth2 credentials and obtain a refresh token (Python, Node, and browser). ## Overview The Box resource uses OAuth2 against the [Box v2 API](https://developer.box.com/reference/) to: * **`/folders/{id}/items`**, list folder items (root id is `0`) * **`/files/{id}/content`**, download file bytes * **`/files/{id}`** / **`/folders/{id}`**, fetch entry metadata * **`/search`**, search files by name/content * **`/oauth2/token`**, refresh access tokens Box is similar to Dropbox in shape but uses **numeric folder/file IDs** rather than paths. Mirage caches the path → ID mapping in the index store, so once you `ls /box/` the IDs are remembered and subsequent reads don't pay the lookup cost. Three credential modes are supported: * **Developer token**, fastest first-run path. Click one button in the Box app console, paste the 32-char access token, and you're done. Lives 60 minutes; you regenerate by hand. No client\_id / refresh\_token / OAuth flow needed. Recommended for `mirage` exploration. * **Code flow with client secret**, for Node/server long-running use. Needs `client_id`, `client_secret`, `refresh_token`. * **PKCE flow**, for browsers. Needs only `client_id` and `refresh_token` (no secret in the bundle). **Box rotates the refresh token on every refresh.** The token you copy out of the initial code exchange is only valid until the next refresh, after that, only the new rotated token works. The `BoxTokenManager` keeps the latest token in memory; pass `onRefreshTokenRotated` if you want to persist it across restarts (the browser example does this with `localStorage`). ## Quick Start: Developer Token (60 minutes, no OAuth) If you just want to try the resource against your own Box account, the developer token is the fastest path, no redirect URI, no client secret, no curl exchange: 1. Go to [https://app.box.com/developers/console](https://app.box.com/developers/console) -> **Create New App** -> **Custom App** -> **User Authentication (OAuth 2.0)** -> name it "Mirage" -> **Create App** 2. On the **Configuration** tab, scroll to the **Developer Token** section near the bottom. 3. Click **Generate Developer Token**. Copy the 32-char token. 4. Paste into your `.env.development`: ```bash theme={null} BOX_DEVELOPER_TOKEN=... ``` 5. Run any of the examples in [`examples/typescript/box/`](https://github.com/strukto-ai/mirage/tree/main/examples/typescript/box). The token expires in **60 minutes** and there is no programmatic way to renew it; you regenerate by hand. For long-running processes, use the OAuth flow below instead. When `BOX_DEVELOPER_TOKEN` is set, the `BoxResource` skips the OAuth refresh path entirely and calls Box directly with the token in the `Authorization: Bearer` header. ## Setup (long-running OAuth flow) ### 1. Create a Box App 1. Go to [https://app.box.com/developers/console](https://app.box.com/developers/console) 2. Click **Create New App** 3. Choose **Custom App** 4. **Authentication Method**: **User Authentication (OAuth 2.0)** 5. Name it (e.g. "Mirage") -> **Create App** ### 2. Configure the App Open the app's **Configuration** tab: * **OAuth 2.0 Redirect URIs**: add every destination you'll redirect back to. Box accepts multiple values, register them all up front so you don't bounce between dev and prod: * For the CLI flow below: `http://localhost:1` * For the browser PKCE example: `http://localhost:5173/box_pkce.html` * For production: e.g. `https://yourapp.com/box/callback` * **Allowed Origins** (only needed if you'll call from a browser; older Box docs call this "CORS Domains"): * `http://localhost:5173` * Any production origins (e.g. `https://yourapp.com`) * **Application Scopes**: * **Read all files and folders stored in Box** (required) * **Write all files and folders stored in Box** (only if you'll add write commands later) * Click **Save Changes** ### 3. Submit for Authorization (enterprise apps only) If your Box account is part of an enterprise, the **Authorization** tab requires an admin to approve the app before it can issue tokens. Personal/free Box accounts skip this step. ### 4. Copy the Client Credentials Still on **Configuration**, copy: * **Client ID** -> `BOX_CLIENT_ID` * **Client Secret** -> `BOX_CLIENT_SECRET` (skip if you only need PKCE) ### 5. Get the Refresh Token (CLI flow with client secret) **A) Open this URL in a browser** (replace `YOUR_CLIENT_ID`): ``` https://account.box.com/api/oauth2/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=http://localhost:1 ``` **B) Authorize:** sign in -> click **Grant access**. **C) Copy the code:** browser redirects to `http://localhost:1?code=...` (page won't load, expected). Copy the `code` from the URL bar. **D) Exchange the code:** ```bash theme={null} curl https://api.box.com/oauth2/token \ -d "code=THE_CODE" \ -d "grant_type=authorization_code" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=YOUR_CLIENT_SECRET" \ -d "redirect_uri=http://localhost:1" ``` Response shape: ```json theme={null} { "access_token": "T...", "expires_in": 3884, "refresh_token": "R...", "restricted_to": [], "token_type": "bearer" } ``` Save the `refresh_token`. ### 6. Set Environment Variables ```bash theme={null} # .env.development BOX_CLIENT_ID=... BOX_CLIENT_SECRET=... BOX_REFRESH_TOKEN=... ``` ## Alternative: PKCE Flow (browser, no client secret) 1. Add `http://localhost:5173/box_pkce.html` to **OAuth 2.0 Redirect URIs** 2. Add `http://localhost:5173` to **Allowed Origins** 3. Set only `BOX_CLIENT_ID` in `.env.development` 4. From `examples/typescript/browser/`, run `pnpm dev` 5. Open `http://localhost:5173/box_pkce.html` and click **Connect Box** The example persists the rotated refresh token to `localStorage` via the `onRefreshTokenRotated` callback so subsequent page loads work without re-auth. ## Token Lifetime | Token | Lifetime | | ------------- | -------------------------------------------------------- | | Access token | \~1 hour (`expires_in: ~3600`) | | Refresh token | 60 days from issue, OR 60 days from last use (whichever) | A refresh token gets revoked when: * 60 days pass without use * The user revokes the app at [https://app.box.com/account/security](https://app.box.com/account/security) * The Box account password changes * A new refresh token is issued (the old one expires after a short grace period) The `BoxTokenManager` always uses the latest token. For long-running processes restart-safe, provide `onRefreshTokenRotated` to persist the new token to disk / a vault. ## CORS Notes Unlike Dropbox, Box has first-class CORS but **only for origins you've explicitly allowlisted** in the **Allowed Origins** section of the app configuration. If you see browser-side `Access-Control-Allow-Origin` errors, double-check the origin matches exactly (no trailing slash; protocol matters). ## Troubleshooting | Issue | Fix | | -------------------------------------------- | ------------------------------------------------------------------------------------------------- | | `400 invalid_grant` on token exchange | The `code` is single-use and short-lived, redo Step 5A with a fresh URL | | `403 access_denied` | Enterprise admin hasn't approved the app on the **Authorization** tab | | `401 unauthorized` after a few hours | Refresh token rotation, the original token is invalid; persist via `onRefreshTokenRotated` | | Browser: `CORS error` even though origin set | Check that the origin in **Allowed Origins** is exact (no trailing slash, http vs https) | | `path/not_found` on `ls /box//` | The folder name was constructed wrong, use `ls /box/` first, then drill into a name from the list | For TypeScript usage: ```ts theme={null} import { BoxResource } from '@struktoai/mirage-node' const box = new BoxResource({ clientId: process.env.BOX_CLIENT_ID!, clientSecret: process.env.BOX_CLIENT_SECRET!, refreshToken: process.env.BOX_REFRESH_TOKEN!, }) ``` Or browser (PKCE): ```ts theme={null} import { BoxResource } from '@struktoai/mirage-browser' const box = new BoxResource({ clientId: 'YOUR_CLIENT_ID', refreshToken: refreshTokenFromLocalStorage, onRefreshTokenRotated: (next) => localStorage.setItem('box-refresh', next), }) ``` For Python usage (same credentials), see [Box (Python)](/python/resource/box): ```python theme={null} from mirage.resource.box import BoxConfig, BoxResource resource = BoxResource(BoxConfig( client_id=os.environ["BOX_CLIENT_ID"], client_secret=os.environ["BOX_CLIENT_SECRET"], refresh_token=os.environ["BOX_REFRESH_TOKEN"], )) ``` # Ceph (Rados Gateway) Source: https://docs.mirage.strukto.ai/home/setup/ceph Set up Ceph Rados Gateway (RGW) credentials for the Ceph resource. ## Credentials ### 1. Create an RGW user On a host with `radosgw-admin` access: ```bash theme={null} radosgw-admin user create --uid=mirage --display-name="Mirage" ``` The output includes a `keys` block with `access_key` and `secret_key`. Note your gateway **endpoint** (e.g. `https://ceph.example.com`) and the **bucket** name. Ceph RGW is self-hosted, so there is no region-derived endpoint, the `endpoint_url` is required and RGW uses path-style addressing. ### 2. Set environment variables ```bash theme={null} # .env.development CEPH_BUCKET=my-bucket CEPH_ENDPOINT_URL=https://ceph.example.com CEPH_ACCESS_KEY_ID=... CEPH_SECRET_ACCESS_KEY=... ``` For Python configuration, see the [Ceph resource](/python/resource/ceph) docs. # Chroma Source: https://docs.mirage.strukto.ai/home/setup/chroma Prepare a ChromaDB collection for the Chroma resource. ## Requirements Mirage mounts an existing ChromaDB collection as a read-only virtual filesystem. The collection must already contain: * One path tree document with ID `__path_tree__`. * One or more page chunk documents. * A metadata field that maps each chunk to a virtual file path slug. * A metadata field that orders chunks within each file. The Python resource connects via Chroma's `AsyncHttpClient`. ## Path Tree Document The path tree document defines the filesystem tree. Store it in Chroma with the ID `__path_tree__`. Its document content must be either plain JSON or gzip-compressed base64 JSON. ```json theme={null} { "README.md": { "size": 1024, "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-02T00:00:00Z" }, "guides/quickstart.md": { "size": 4096, "updated_at": "2026-01-03T00:00:00Z" } } ``` Path tree keys become file paths below the Mirage mount. Use forward slashes to create directories. Path rules: * Do not use an empty path. * Do not use `.`, `..`, or empty path segments. * Keep every path unique after trimming leading and trailing slashes. * Do not use a path both as a file and as a directory prefix. For example, `guides` and `guides/quickstart.md` cannot both be files. Metadata fields are optional. Mirage uses `size`, `created_at`, and `updated_at` when present. ## Chunk Documents Each file is materialized from one or more Chroma documents. Every chunk must include metadata that identifies the file slug and chunk order: ```json theme={null} { "page_slug": "guides/quickstart.md", "chunk_index": 0 } ``` Default metadata fields: | Field | Default | Purpose | | ----------------- | ------------- | --------------------------------------- | | Slug field | `page_slug` | File path in the virtual tree | | Chunk index field | `chunk_index` | Sort order when assembling file content | If your collection uses different metadata names, configure `ChromaConfig(slug_field="...", chunk_index_field="...")`. ## Environment Variables ```bash theme={null} # .env.development CHROMA_COLLECTION=knowledge CHROMA_HOST=localhost CHROMA_PORT=8000 CHROMA_SSL=false CHROMA_SLUG_FIELD=page_slug CHROMA_CHUNK_INDEX_FIELD=chunk_index CHROMA_EXAMPLE_QUERY=getting started ``` ## Validate the Collection Before mounting, verify that the collection contains the path tree document and at least one chunk with matching slug metadata: ```python theme={null} import chromadb client = chromadb.HttpClient(host="localhost", port=8000) collection = client.get_collection("knowledge") tree = collection.get(ids=["__path_tree__"]) chunks = collection.get(where={"page_slug": "guides/quickstart.md"}) print(tree["documents"][0]) print(len(chunks["documents"])) ``` For Python configuration, see the [Python Chroma Setup](/python/setup/chroma) guide. # Databricks Volume Source: https://docs.mirage.strukto.ai/home/setup/databricks Set up Databricks credentials for the Unity Catalog volume resource. ## Credentials The resource reads a Unity Catalog volume over the Databricks SDK. It needs a workspace host plus one auth method: a personal access token, a config profile, or the SDK defaults (handy inside Databricks Apps). ### 1. Get Your Workspace Host Your workspace URL, e.g. `https://dbc-xxxxxxxx-xxxx.cloud.databricks.com` (AWS) or `https://adb-xxxxxxxxxxxx.xx.azuredatabricks.net` (Azure). ### 2. Create a Personal Access Token 1. In the workspace, open **Settings** -> **Developer** -> **Access tokens**. 2. Click **Generate new token**, set a comment and lifetime, and copy it. ### 3. Set Environment Variables ```bash theme={null} # .env.development DATABRICKS_HOST=https://dbc-xxxxxxxx-xxxx.cloud.databricks.com DATABRICKS_TOKEN=dapi... ``` ### Alternatives * **Config profile:** add a profile to `~/.databrickscfg` and pass `profile="DEV"` instead of host/token. * **SDK defaults:** omit host, token, and profile to fall through to the Databricks SDK default auth chain (for example inside Databricks Apps). For Python configuration, see the [Python Databricks Volume Setup](/python/setup/databricks) guide. # Dify Source: https://docs.mirage.strukto.ai/home/setup/dify Set up a Dify Knowledge API key and dataset for the Dify resource. ## Credentials Mirage uses the Dify Knowledge Base API. You need a dataset API key and the dataset ID for the knowledge base you want to mount. ### 1. Get a Knowledge API Key 1. Open your Dify workspace. 2. Go to the target knowledge base. 3. Open **API Access** or the dataset API settings. 4. Create or copy an API key with permission to read documents, segments, and retrieval results. The key is sent as a bearer token to the Dify API. ### 2. Find the Dataset ID Use the dataset ID from the knowledge base URL or Dify API settings. The ID is used in API paths such as: ```text theme={null} /datasets//documents ``` ### 3. Set Environment Variables ```bash theme={null} # .env.development DIFY_API_KEY=dataset-... DIFY_BASE_URL=https://api.dify.ai/v1 DIFY_DATASET_ID=... DIFY_SLUG_METADATA_NAME=slug ``` For self-hosted Dify, set `DIFY_BASE_URL` to your API base URL, for example: ```bash theme={null} DIFY_BASE_URL=https://dify.example.com/v1 ``` ## Document Metadata Mirage can mount a Dify dataset without custom metadata, but scoped search works best when every document has a stable slug metadata field. Mirage looks for a field named `slug` by default; Python users can change that field name with `DifyConfig(slug_metadata_name="...")`. ### Recommended: Add a Slug Metadata Field Create a metadata field on the Dify knowledge dataset and use the value as the virtual path below the Mirage mount. The default field name is `slug`. | Dify document | `slug` metadata value | Mirage path | | -------------- | ---------------------- | --------------------------------- | | Quickstart | `guides/quickstart.md` | `/knowledge/guides/quickstart.md` | | Support Policy | `policies/support.md` | `/knowledge/policies/support.md` | If your Dify metadata field is named `path`, set `slug_metadata_name="path"` in the Dify resource config and use `name: path` in Dify metadata. Guidelines: * Use forward slashes to create folders. * Do not start with `/`; Mirage normalizes leading and trailing slashes. * Do not use empty segments, `.`, or `..`. * Keep values unique. Two documents with the same slug metadata value will raise a duplicate path error. * Avoid making one document's slug metadata value a parent of another document's slug metadata value. For example, do not use both `guides` and `guides/quickstart.md`; Mirage treats that as a path collision. ### Add or Update Slug Metadata in Dify In Dify's Knowledge UI, open each document and edit its metadata. Add a custom metadata item: ```text theme={null} name: slug value: guides/quickstart.md ``` If you upload or sync documents programmatically, include the same metadata on the document. Mirage reads Dify's `doc_metadata` field and looks for an item whose `name` matches `slug_metadata_name`. ### Enable Built-in Fields for Name-Based Documents If a document does not have the configured slug metadata field, Mirage falls back to the Dify document name for its file path. Scoped `search` can still target those name-based documents, but Dify must expose the `document_name` built-in field for metadata filtering. In the Dify knowledge dataset settings: 1. Open the dataset metadata settings. 2. Enable **Built-in Fields**. 3. Ensure `document_name` is available for metadata filtering. Without Built-in Fields, commands like `ls`, `find`, `cat`, and `grep` still work because Mirage resolves files through the document list and segment APIs. Scoped `search` against name-based paths may return no records because Dify cannot filter retrieval by `document_name`. For the most predictable behavior, add the configured slug metadata field to every document and use slug-based paths. For Python configuration, see the [Python Dify Setup](/python/setup/dify) guide. # DigitalOcean Spaces Source: https://docs.mirage.strukto.ai/home/setup/digitalocean Set up DigitalOcean Spaces credentials for the DigitalOcean resource. ## Credentials ### 1. Create a Spaces access key 1. Sign in to the [DigitalOcean console](https://cloud.digitalocean.com/) 2. Go to **API** -> **Spaces Keys** -> **Generate New Key** 3. Copy the **Access Key** and **Secret** 4. Note your Space's **region** (e.g. `nyc3`) and **Space name** (the bucket) The endpoint is derived from `region` as `.digitaloceanspaces.com`. ### 2. Set environment variables ```bash theme={null} # .env.development DO_SPACE=my-space DO_REGION=nyc3 DO_ACCESS_KEY_ID=... DO_SECRET_ACCESS_KEY=... ``` For Python configuration, see the [DigitalOcean resource](/python/resource/digitalocean) docs. # Discord Source: https://docs.mirage.strukto.ai/home/setup/discord Set up a Discord Bot Token for the Discord resource. ## Credentials ### 1. Create a Discord Application 1. Go to [https://discord.com/developers/applications](https://discord.com/developers/applications) 2. **New Application** -> name it (e.g., "Mirage") ### 2. Create a Bot 1. **Bot** in the left sidebar 2. **Reset Token** -> copy the **Bot Token** 3. Under **Privileged Gateway Intents**, enable: * **Message Content Intent** (required to read message content) ### 3. Invite the Bot to Your Server 1. **OAuth2** -> **URL Generator** in the left sidebar 2. Select scopes: `bot` 3. Select bot permissions: `Read Messages/View Channels`, `Read Message History` 4. Copy the generated URL and open it in a browser to invite ### 4. Set Environment Variables ```bash theme={null} # .env.development DISCORD_BOT_TOKEN=MTIx... ``` For Python configuration, see the [Python Discord Setup](/python/setup/discord) guide. # Dropbox Source: https://docs.mirage.strukto.ai/home/setup/dropbox Set up Dropbox OAuth2 credentials and obtain a long-lived refresh token. ## Overview The Dropbox resource uses OAuth2 with a long-lived refresh token to authenticate against the [Dropbox v2 HTTP API](https://www.dropbox.com/developers/documentation/http/documentation): * **`/2/files/list_folder`**, list folder entries * **`/2/files/download`**, download file bytes * **`/2/files/search_v2`**, search files by name/content * **`/2/files/get_metadata`**, fetch entry metadata Two flows are supported: * **Code flow with client secret**, for Node/server. Needs `client_id`, `client_secret`, `refresh_token`. * **PKCE flow**, for browsers. Needs only `client_id` and `refresh_token` (no secret in the bundle). Both produce the same long-lived refresh token; the resource auto-refreshes short-lived access tokens (≈4h) behind the scenes. ## Setup ### 1. Create a Dropbox App 1. Go to [https://www.dropbox.com/developers/apps](https://www.dropbox.com/developers/apps) 2. Click **Create app** 3. Choose: * API: **Scoped access** * Access type: **App folder** (sandboxed to one folder Dropbox creates for you) or **Full Dropbox** (your entire account). Pick App folder for least privilege. * Name it (e.g., "Mirage") -> **Create app** ### 2. Configure Permissions On the app's settings page, click the **Permissions** tab and check: * `files.metadata.read`, list folders, read file metadata * `files.content.read`, download file bytes If you also want write/delete (not used by the read-only resource yet), check `files.content.write` and `files.metadata.write`. Click **Submit** at the bottom of the Permissions tab. **Required**: Dropbox does not include unchecked scopes in tokens, even if the auth URL requests them. ### 3. Configure OAuth 2 Redirect URI On the **Settings** tab, scroll to **OAuth 2 -> Redirect URIs** and add: * For the CLI flow below: `http://localhost:1` * For the browser PKCE example: `http://localhost:5173/dropbox_pkce.html` Click **Add** after each. ### 4. Copy the App Key (and Secret) Still on **Settings**, copy: * **App key**, this is your `DROPBOX_APP_KEY` * **App secret**, click **Show** -> copy. This is your `DROPBOX_APP_SECRET`. **Skip if you only need the PKCE flow.** ### 5. Get the Refresh Token (CLI flow with client secret) This mirrors the Google flow, easiest path for Node/server use. **A) Open this URL in a browser** (replace `YOUR_APP_KEY`): ``` https://www.dropbox.com/oauth2/authorize?client_id=YOUR_APP_KEY&response_type=code&token_access_type=offline&redirect_uri=http://localhost:1 ``` * `token_access_type=offline`, **required** to receive a refresh token. Without this you only get a short-lived access token. **B) Authorize:** sign in -> click **Allow**. **C) Copy the code:** the browser redirects to `http://localhost:1?code=ABC...` (the page won't load, expected). Copy the `code` value from the URL bar. **D) Exchange the code for a refresh token:** ```bash theme={null} curl https://api.dropboxapi.com/oauth2/token \ -d "code=THE_CODE_FROM_STEP_C" \ -d "grant_type=authorization_code" \ -d "client_id=YOUR_APP_KEY" \ -d "client_secret=YOUR_APP_SECRET" \ -d "redirect_uri=http://localhost:1" ``` The response contains `refresh_token`, save it. Example: ```json theme={null} { "access_token": "sl.B...", "expires_in": 14400, "refresh_token": "abc123...", "scope": "files.metadata.read files.content.read", "token_type": "bearer", "uid": "12345", "account_id": "dbid:..." } ``` ### 6. Set Environment Variables ```bash theme={null} # .env.development DROPBOX_APP_KEY=abc123app4key DROPBOX_APP_SECRET=def456app4secret DROPBOX_REFRESH_TOKEN=ghi789refresh4token ``` ## Alternative: PKCE Flow (browser, no client secret) If you're mounting Dropbox from a browser SPA and don't want to ship a client secret, use the PKCE flow. The bundled example does the dance end-to-end. 1. In the Dropbox app **Settings** tab, ensure the redirect URI `http://localhost:5173/dropbox_pkce.html` is registered (Step 3). 2. Set only `DROPBOX_APP_KEY` in `.env.development` at the repo root (no secret needed). 3. From `examples/typescript/browser/`, run `pnpm dev`. 4. Open `http://localhost:5173/dropbox_pkce.html` and click **Connect Dropbox**. The example persists the refresh token to `localStorage`, then mounts a `DropboxResource` with just `{ clientId, refreshToken }` and runs `ls /dropbox/`. Inspect DevTools -> Network -> filter `token` to confirm refresh calls don't include `client_secret`. The same `refresh_token` can be reused for headless setups, copy it out of `localStorage` and set it as `DROPBOX_REFRESH_TOKEN` in your env. ## Token Lifetime | Token | Lifetime | | ------------- | ------------------------------------------- | | Access token | \~4 hours (`expires_in: 14400`) | | Refresh token | Long-lived; survives until manually revoked | Refresh tokens get revoked if: * The user revokes the app at [https://www.dropbox.com/account/connected\_apps](https://www.dropbox.com/account/connected_apps) * The Dropbox account password is changed * The app is deleted in the developer console The `DropboxTokenManager` in the resource transparently exchanges the refresh token for a fresh access token whenever the cached one is within 5 minutes of expiry. ## Scopes Reference | Scope | Purpose | | ---------------------- | ------------------------------------ | | `files.metadata.read` | List folders, read file metadata | | `files.metadata.write` | Move, rename, create folders | | `files.content.read` | Download file bytes | | `files.content.write` | Upload, overwrite, delete files | | `sharing.read` | List shared links and shared folders | | `account_info.read` | Account email, country, user info | The Mirage resource only needs `files.metadata.read` + `files.content.read` for the read-only mount. Add the `*.write` scopes if/when write commands land. ## Troubleshooting | Issue | Fix | | ------------------------------------------- | --------------------------------------------------------------------------------------------------- | | `400 invalid_grant` on token exchange | The `code` is single-use and expires fast, re-do Step 5A with a fresh URL | | No `refresh_token` in token response | Add `token_access_type=offline` to the auth URL | | `path/not_found/.` on `ls /dropbox/` | Account is empty, or app is **App folder** scoped and the sandbox folder hasn't been created | | `missing_scope` on download | Check `files.content.read` is enabled in the **Permissions** tab and **submitted** | | Browser PKCE: redirect URI mismatch | Dropbox is strict, `http://localhost:5173/dropbox_pkce.html` must match exactly (no trailing slash) | | Token works locally but fails after a while | Access token expired, the `TokenManager` should auto-refresh; if not, refresh token may be revoked | For TypeScript usage, mount with: ```ts theme={null} import { DropboxResource } from '@struktoai/mirage-node' const dropbox = new DropboxResource({ clientId: process.env.DROPBOX_APP_KEY!, clientSecret: process.env.DROPBOX_APP_SECRET!, refreshToken: process.env.DROPBOX_REFRESH_TOKEN!, }) ``` Or browser (PKCE): ```ts theme={null} import { DropboxResource } from '@struktoai/mirage-browser' const dropbox = new DropboxResource({ clientId: 'YOUR_APP_KEY', refreshToken: refreshTokenFromLocalStorage, }) ``` # Email Source: https://docs.mirage.strukto.ai/home/setup/email Set up IMAP/SMTP credentials for the Email resource. ## Credentials The Email resource connects to any email account via IMAP (reading) and SMTP (sending). You need the server hostnames and a password or app password. ### 1. Find Your IMAP/SMTP Settings | Resource | IMAP Host | IMAP Port | SMTP Host | SMTP Port | | ----------- | ----------------------- | --------- | --------------------- | --------- | | Outlook/365 | `outlook.office365.com` | 993 | `smtp.office365.com` | 587 | | Yahoo | `imap.mail.yahoo.com` | 993 | `smtp.mail.yahoo.com` | 587 | | Fastmail | `imap.fastmail.com` | 993 | `smtp.fastmail.com` | 587 | | iCloud | `imap.mail.me.com` | 993 | `smtp.mail.me.com` | 587 | | ProtonMail | `127.0.0.1` | 1143 | `127.0.0.1` | 1025 | | Self-hosted | Your server hostname | 993 | Your server hostname | 587 | ProtonMail requires [ProtonMail Bridge](https://proton.me/mail/bridge) running locally. ### 2. Create an App Password Most resources require an **app password** instead of your regular account password. This is a one-time password specifically for third-party apps. **Outlook / Microsoft 365:** 1. Go to [https://account.microsoft.com/security](https://account.microsoft.com/security) 2. **Security** -> **Advanced security options** 3. **App passwords** -> **Create a new app password** 4. Copy the generated password **Yahoo:** 1. Go to [https://login.yahoo.com/account/security](https://login.yahoo.com/account/security) 2. **Generate app password** 3. Select **Other App**, name it (e.g., "Mirage") 4. Copy the generated password **Fastmail:** 1. Go to [https://www.fastmail.com/settings/security/tokens](https://www.fastmail.com/settings/security/tokens) 2. **New App Password** 3. Select **IMAP/SMTP** access 4. Copy the generated password **iCloud:** 1. Go to [https://appleid.apple.com/account/manage](https://appleid.apple.com/account/manage) 2. **Sign-In and Security** -> **App-Specific Passwords** 3. **Generate an app-specific password** 4. Copy the generated password **Self-hosted / Other:** Use your regular email password, or whatever your mail server requires. ### 3. Set Environment Variables ```bash theme={null} # .env.development IMAP_HOST=imap.fastmail.com SMTP_HOST=smtp.fastmail.com EMAIL_USERNAME=you@fastmail.com EMAIL_PASSWORD=your-app-password ``` ## Troubleshooting **"Authentication failed"** - Make sure you're using an app password, not your regular account password. Most resources block regular passwords for IMAP/SMTP access. **"Connection refused"** - Check the hostname and port. Some networks block port 993/587. For ProtonMail, make sure Bridge is running. **"SSL handshake failed"** - If your server uses STARTTLS on port 143 instead of SSL on port 993, set `use_ssl=False` and `imap_port=143`. For Python configuration, see the [Python Email Setup](/python/setup/email) guide. # Support Matrix Source: https://docs.mirage.strukto.ai/home/setup/fuse Which OS and SDK combinations support MIRAGE FUSE mounts, and how well. FUSE mode exposes a mount as a real OS directory so any tool (editors, sandbox runtimes, plain `cat`) can read it, not just MIRAGE commands. OS support depends on the SDK's FUSE binding: Python uses [mfusepy](https://github.com/mxmlnkn/mfusepy) (ctypes over libfuse), Node uses [@zkochan/fuse-native](https://www.npmjs.com/package/@zkochan/fuse-native) (a native addon), and browsers cannot mount filesystems at all. ## Matrix | OS | Python | TypeScript (Node) | Notes | | ------- | --------------------- | --------------------- | ---------------------------------------------------------------- | | Linux | ✅ supported, CI-gated | ✅ supported, CI-gated | `fuse3`; multiple mounts per process | | macOS | ✅ supported | ✅ supported | macFUSE kernel extension; **one mount per process** | | Windows | 🧪 experimental | ❌ not supported | Python via [WinFsp](/home/setup/windows); advisory CI job passes | | Browser | ❌ | ❌ | no kernel; use the virtual executor instead | Per-OS install guides: [macOS](/home/setup/macos), [Linux](/home/setup/linux), [Windows](/home/setup/windows). SDK wiring: [Python FUSE setup](/python/setup/fuse), [TypeScript FUSE setup](/typescript/setup/fuse). ## What the labels mean * **Supported, CI-gated (Linux).** The FUSE integration battery (both SDKs, real kernel mounts, including size-unknown API files) runs on every change and gates merges. * **Supported (macOS).** Same code paths, verified on real macFUSE kext mounts; hosted CI runners cannot approve kernel extensions, so macOS coverage is local rather than gated. Remember the one-mount-per-process limit. * **Experimental (Windows, Python only).** The full battery passes over WinFsp in an advisory CI job (not merge-gating). Windows conventions (unmount at process exit, mount-level ownership, stat-opens-a-handle) are documented on the [Windows page](/home/setup/windows). Write-heavy flows and symlinks are not yet exercised there. * **Not supported (Windows, TypeScript).** `@zkochan/fuse-native` only targets macOS and Linux; its legacy Windows path builds against the unmaintained Dokany-based `fuse-shared-library-win32` rather than WinFsp. ## Platform quirks at a glance | Quirk | Linux | macOS | Windows | | --------------------------- | ------------------------------ | --------------------------------- | --------------------------------- | | Mounts per process | many | **one** (Python and Node) | many | | Unmount | `fusermount -u` / `ws.close()` | `diskutil unmount` / `ws.close()` | at process exit only | | Size-unknown files pre-open | stat 0 | stat 0 | stat fetches, shows real size | | Mountpoint directory | must exist | must exist | must **not** exist (auto-created) | The size-unknown semantics themselves (stat 0 until open, full content on read, real size after open) are identical across SDKs; see [Python](/python/setup/fuse#size-semantics-for-api-backed-files) or [TypeScript](/typescript/limitations#3-size-unknown-api-files-stat-as-0-bytes-until-first-open) for the per-tool table. # Google Cloud Storage Source: https://docs.mirage.strukto.ai/home/setup/gcs Set up Google Cloud Storage credentials for the GCS resource. ## Credentials ### 1. Create HMAC Keys 1. Go to [https://console.cloud.google.com/storage/settings](https://console.cloud.google.com/storage/settings) -> **Interoperability** 2. Under **Service account HMAC**, click **Create a key for a service account** 3. Select a service account with Storage Object Viewer (read) or Storage Object Admin (read/write) 4. Copy the **Access key** and **Secret** ### 2. Set Environment Variables ```bash theme={null} # .env.development GCS_BUCKET=mirage-ai GCS_ACCESS_KEY_ID=GOOG... GCS_SECRET_ACCESS_KEY=... ``` For Python configuration, see the [GCS resource](/python/resource/gcs) docs. # GitHub Source: https://docs.mirage.strukto.ai/home/setup/github Set up a GitHub Personal Access Token for the GitHub resource. ## Credentials ### 1. Create a Personal Access Token 1. Go to [https://github.com/settings/tokens](https://github.com/settings/tokens) 2. **Generate new token (classic)** or **Fine-grained token** 3. For classic tokens, select scopes: * `repo` (read access to repositories) 4. For fine-grained tokens: * **Repository access**: select the repos you need * **Permissions**: Contents -> Read-only 5. Copy the token ### 2. Set Environment Variables ```bash theme={null} # .env.development GITHUB_TOKEN=ghp_xxxx... ``` For Python configuration, see the [Python GitHub Setup](/python/setup/github) guide. # Google Workspace Source: https://docs.mirage.strukto.ai/home/setup/google Set up Google OAuth2 credentials for Docs, Sheets, Slides, Drive, and Gmail resources. ## Overview The Google resources use OAuth2 with a refresh token to authenticate against: * **Google Drive API v3** - lists documents, reads metadata * **Google Docs API v1** - reads/writes document content Authentication requires three values: `client_id`, `client_secret`, and `refresh_token`. ## Setup ### 1. Create a Google Cloud Project 1. Go to [https://console.cloud.google.com/](https://console.cloud.google.com/) 2. Click the project selector dropdown -> **New Project** 3. Name it (e.g., "Mirage") -> **Create** 4. Select it from the dropdown ### 2. Enable APIs 1. Go to [https://console.cloud.google.com/apis/library](https://console.cloud.google.com/apis/library) 2. Search **Google Drive API** -> click -> **Enable** 3. Search **Google Docs API** -> click -> **Enable** ### 3. Configure Google Auth Platform The OAuth settings are under **Google Auth Platform** in the Cloud Console left sidebar (or go to [https://console.cloud.google.com/auth/overview](https://console.cloud.google.com/auth/overview)). **A) Branding** (left sidebar): 1. Fill in: App name (e.g., "Mirage"), support email, developer contact email 2. Save **B) Audience** (left sidebar): 1. Set user type to **External** 2. Add your own Google email as a **test user** 3. Save **C) Data Access** (left sidebar) - this is where scopes are configured: 1. Click **Add or Remove Scopes** 2. Search for or paste these scopes: * `https://www.googleapis.com/auth/drive` (full Drive access) * `https://www.googleapis.com/auth/documents` (Google Docs) * `https://www.googleapis.com/auth/presentations` (Google Slides) 3. Select them -> Save **D) Publish** (to avoid 7-day token expiry): 1. Go to **Audience** -> click **Publish App** 2. See [Token Lifetime](#token-lifetime) for details ### 4. Create OAuth2 Client 1. Go to **Clients** in the left sidebar (or click **Create OAuth client** on the Overview page) 2. Application type: **Desktop app** 3. Name it -> **Create** 4. Copy the **Client ID** and **Client Secret** ### 5. Get the Refresh Token **A) Open this URL in a browser** (replace `YOUR_CLIENT_ID`): ``` https://accounts.google.com/o/oauth2/v2/auth?client_id=YOUR_CLIENT_ID&redirect_uri=http://localhost:1&response_type=code&scope=https://www.googleapis.com/auth/drive%20https://www.googleapis.com/auth/documents%20https://www.googleapis.com/auth/presentations&access_type=offline&prompt=consent ``` * `access_type=offline` - required to receive a refresh token * `prompt=consent` - forces refresh token issuance on re-auth **B) Authorize:** Sign in -> click through "unverified app" warning -> grant permissions. **C) Copy the code:** The browser redirects to `http://localhost:1?code=4/0AXXXX...` (page won't load - expected). Copy the `code` value from the URL bar. **D) Exchange for tokens:** ```bash theme={null} curl -X POST https://oauth2.googleapis.com/token \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=YOUR_CLIENT_SECRET" \ -d "code=THE_CODE_FROM_STEP_C" \ -d "grant_type=authorization_code" \ -d "redirect_uri=http://localhost:1" ``` The response JSON contains `refresh_token` - save it. ### 6. Set Environment Variables ```bash theme={null} # .env.development GOOGLE_CLIENT_ID=123456789-abc.apps.googleusercontent.com GOOGLE_CLIENT_SECRET=GOCSPx-xxx GOOGLE_REFRESH_TOKEN=1//0abc... ``` ## Token Lifetime | App Publishing Status | Refresh Token Lifetime | | --------------------- | ---------------------------------- | | Testing (default) | 7 days - must re-authorize | | In Production | Never expires (with continued use) | To publish: go to the OAuth consent screen page and click **Publish App**. Users will see an "unverified app" warning during auth, which is fine for personal use. A published refresh token only gets revoked if: * Manually revoked at [https://myaccount.google.com/permissions](https://myaccount.google.com/permissions) * Google account password is changed * Token unused for 6 months * 100+ outstanding refresh tokens per account per client (oldest revoked) For personal use with a published app, you do the auth flow **once** and it works indefinitely. The `TokenManager` in the resource automatically uses the refresh token to obtain fresh access tokens (which expire hourly) behind the scenes. ## Scopes Reference | Scope | Purpose | | ---------------------------------------------------- | ---------------------------------------------- | | `https://www.googleapis.com/auth/drive` | Full Drive access (list, create, delete files) | | `https://www.googleapis.com/auth/drive.readonly` | List files, read metadata only (alternative) | | `https://www.googleapis.com/auth/documents` | Read and write Google Docs | | `https://www.googleapis.com/auth/documents.readonly` | Read-only Docs access (alternative) | | `https://www.googleapis.com/auth/presentations` | Read and write Google Slides | ## Troubleshooting | Issue | Fix | | ---------------------------------- | ----------------------------------------------------------------- | | 403 during OAuth flow | Add yourself as a test user in Step 3.7 | | No `refresh_token` in response | Ensure `access_type=offline` and `prompt=consent` in the auth URL | | Token exchange fails | `redirect_uri` must exactly match between auth URL and curl | | Refresh token expires after 7 days | Publish the app (Step 3.9) | | Auth code doesn't work twice | Codes are single-use - re-authorize if exchange fails | For Python configuration, see the [Python Google Workspace Setup](/python/setup/google) guide. # GridFS Source: https://docs.mirage.strukto.ai/home/setup/gridfs Set up a MongoDB connection for the GridFS file-storage resource. GridFS stores files inside MongoDB (metadata in `fs.files`, content chunks in `fs.chunks`). The GridFS resource only needs a MongoDB connection URI plus a database name; the connection is set up exactly like the [MongoDB resource](/home/setup/mongodb). ## Credentials ### 1. Get Your Connection URI #### Local MongoDB ```bash theme={null} # Default local instance MONGODB_URI=mongodb://localhost:27017 ``` #### MongoDB Atlas (Cloud) 1. Go to [https://cloud.mongodb.com](https://cloud.mongodb.com) 2. Select your cluster -> **Connect** -> **Drivers** 3. Copy the connection string: ``` mongodb+srv://:@cluster0.xxxxx.mongodb.net/ ``` #### Self-hosted with Authentication ```bash theme={null} MONGODB_URI=mongodb://username:password@host:27017/?authSource=admin ``` ### 2. Set Environment Variables ```bash theme={null} # .env.development MONGODB_URI=mongodb+srv://user:pass@cluster0.xxxxx.mongodb.net/ ``` For usage, see the [Python GridFS resource](/python/resource/gridfs) or the [TypeScript GridFS setup](/typescript/setup/gridfs). # HF Buckets Source: https://docs.mirage.strukto.ai/home/setup/hf_buckets Set up Hugging Face Buckets credentials for the HF Buckets resource. ## Credentials ### 1. Create a Bucket 1. Go to [https://huggingface.co/new-bucket](https://huggingface.co/new-bucket) and create a bucket 2. Note the full name in `namespace/bucket-name` form (e.g. `your-user/my-data`) ### 2. Create an Access Token 1. Go to [https://huggingface.co/settings/tokens](https://huggingface.co/settings/tokens) 2. Create a fine-grained token with **Read** (or **Write** if you need to upload) access to the bucket's namespace ### 3. Set Environment Variables ```bash theme={null} # .env.development HF_BUCKET_NAME=your-user/my-data HF_TOKEN=hf_... ``` For Python configuration, see the [HF Buckets resource](/python/resource/hf_buckets) docs. # HF Datasets Source: https://docs.mirage.strukto.ai/home/setup/hf_datasets Set up Hugging Face Datasets credentials. ## Credentials ### 1. Pick a Dataset Repo Browse [huggingface.co/datasets](https://huggingface.co/datasets) and note the full `namespace/dataset-name` (e.g. `AlienKevin/SWE-ZERO-12M-trajectories`). Public datasets need no token. Gated/private datasets do. ### 2. Create an Access Token (gated/private only) 1. Go to [https://huggingface.co/settings/tokens](https://huggingface.co/settings/tokens) 2. Create a fine-grained token with **Read** access to the dataset ### 3. Set Environment Variables ```bash theme={null} # .env.development HF_DATASET_REPO=AlienKevin/SWE-ZERO-12M-trajectories HF_TOKEN=hf_... # optional, public datasets work without it ``` For Python configuration, see the [HF Datasets resource](/python/resource/hf_datasets) docs. # HF Models Source: https://docs.mirage.strukto.ai/home/setup/hf_models Set up Hugging Face Models credentials. ## Credentials ### 1. Pick a Model Repo Browse [huggingface.co/models](https://huggingface.co/models) and note the full `namespace/model-name` (e.g. `sapientinc/HRM-Text-1B`). Public models need no token. Gated/private models do. ### 2. Create an Access Token (gated/private only) 1. Go to [https://huggingface.co/settings/tokens](https://huggingface.co/settings/tokens) 2. Create a fine-grained token with **Read** access to the model ### 3. Set Environment Variables ```bash theme={null} # .env.development HF_MODEL_REPO=sapientinc/HRM-Text-1B HF_TOKEN=hf_... # optional, public models work without it ``` For Python configuration, see the [HF Models resource](/python/resource/hf_models) docs. # HF Spaces Source: https://docs.mirage.strukto.ai/home/setup/hf_spaces Set up Hugging Face Spaces credentials. ## Credentials ### 1. Pick a Space Repo Browse [huggingface.co/spaces](https://huggingface.co/spaces) and note the full `namespace/space-name` (e.g. `HuggingFaceBio/carbon-demo`). Public spaces need no token. Private spaces do. ### 2. Create an Access Token (private only) 1. Go to [https://huggingface.co/settings/tokens](https://huggingface.co/settings/tokens) 2. Create a fine-grained token with **Read** access to the space ### 3. Set Environment Variables ```bash theme={null} # .env.development HF_SPACE_REPO=HuggingFaceBio/carbon-demo HF_TOKEN=hf_... # optional, public spaces work without it ``` For Python configuration, see the [HF Spaces resource](/python/resource/hf_spaces) docs. # LanceDB Source: https://docs.mirage.strukto.ai/home/setup/lancedb Set up a LanceDB connection for the LanceDB resource. LanceDB mounts a table as a read-only filesystem: group-by columns become nested folders, each row is a card plus an optional blob file, and semantic search is the `search` command. ## Connection ### Local / embedded Point `uri` at a directory. LanceDB stores the table there, no server needed. ```bash theme={null} # .env.development LANCEDB_URI=/data/fashion.lancedb ``` ### Object storage `uri` also accepts `s3://`, `gs://`, and `az://` prefixes for tables backed by object storage. ### Cloud and Enterprise `db://` URIs connect to LanceDB Cloud (`api_key` + `region`) or Enterprise (`api_key` + `host_override`): ```bash theme={null} # .env.development LANCEDB_URI=db://my-database LANCEDB_API_KEY=sk_... LANCEDB_REGION=us-east-1 # Enterprise only LANCEDB_HOST_OVERRIDE=https://my-database.us-east-1.api.lancedb.com ``` ## Search Search is the `search "" ` command. It returns ranked rows as their canonical `.md` file paths plus a score, so results compose with `cat`, `wc`, and pipes (`grep`/`rg` stay lexical). It requires a table built with an embedding function on a source field, so `tbl.search(text)` auto-embeds the query. ## Limits The resource is read-only and bounds how much an agent can pull: * `search_limit` (10): default top-k returned by `search`. * `max_rows` (1,000): hard ceiling on rows listed per folder. See the [Python](/python/resource/lancedb) and [TypeScript](/typescript/setup/lancedb) resource pages for full config. # Langfuse Source: https://docs.mirage.strukto.ai/home/setup/langfuse Set up Langfuse API keys for the Langfuse resource. ## Credentials ### 1. Get API Keys #### Langfuse Cloud 1. Go to [https://cloud.langfuse.com](https://cloud.langfuse.com) 2. Select your project -> **Settings** -> **API Keys** 3. Copy the **Public Key** and **Secret Key** #### Self-hosted 1. Go to your Langfuse instance (e.g., `https://langfuse.yourcompany.com`) 2. Select your project -> **Settings** -> **API Keys** 3. Copy the **Public Key** and **Secret Key** ### 2. Set Environment Variables ```bash theme={null} # .env.development LANGFUSE_PUBLIC_KEY=pk-lf-... LANGFUSE_SECRET_KEY=sk-lf-... LANGFUSE_HOST=https://cloud.langfuse.com ``` For Python configuration, see the [Python Langfuse Setup](/python/setup/langfuse) guide. # Linear Source: https://docs.mirage.strukto.ai/home/setup/linear Set up a Linear personal API key for the Linear resource. ## Credentials ### 1. Create a Personal API Key 1. Go to Settings -> Security & Access 2. Create a new personal API key 3. Copy the key ### 2. Set Environment Variables ```bash theme={null} # .env.development LINEAR_API_KEY=lin_api_xxxx... ``` For Python configuration, see the [Python Linear Setup](/python/setup/linear) guide. # Linux Source: https://docs.mirage.strukto.ai/home/setup/linux Set up FUSE on Linux for MIRAGE. ## FUSE Setup FUSE mode mounts virtual filesystems as real directories, allowing any tool (not just MIRAGE commands) to access mounted data. ### Install FUSE ```bash theme={null} sudo apt-get install fuse3 libfuse3-dev ``` ```bash theme={null} sudo dnf install fuse3 fuse3-devel ``` ```bash theme={null} sudo pacman -S fuse3 ``` ### Allow User Mounts Edit `/etc/fuse.conf` and uncomment `user_allow_other`: ```bash theme={null} sudo sed -i 's/#user_allow_other/user_allow_other/' /etc/fuse.conf ``` ### Verify ```bash theme={null} fusermount3 --version ``` # macOS Source: https://docs.mirage.strukto.ai/home/setup/macos Set up FUSE on macOS for MIRAGE. ## Install macFUSE ```bash theme={null} brew install --cask macfuse ``` ## Enable Kernel Extension Open **System Settings - Privacy & Security**. Scroll to the bottom, you will see: > "System software from developer 'Benjamin Fleischer' was blocked from loading." Click **Allow**. Restart your Mac. macOS requires a reboot to load the kernel extension. ```bash theme={null} ls /Library/Filesystems/macfuse.fs ``` If this directory exists, macFUSE is installed. ## Apple Silicon (M1/M2/M3/M4) If you don't see the **Allow** button: Shut down your Mac completely. Hold the power button until "Loading startup options" appears. Click **Options - Continue**. In Recovery: **Utilities - Startup Security Utility**. Set to **Reduced Security** and check **"Allow user management of kernel extensions"**. Restart, then go back to **System Settings - Privacy & Security** to allow the extension. # Mem0 Source: https://docs.mirage.strukto.ai/home/setup/mem0 Set up a Mem0 API key and memory scope for the Mem0 resource. ## Credentials Mirage uses the [Mem0 Platform](https://docs.mem0.ai) managed API. You need a Mem0 API key and one memory scope — a `user_id`, `agent_id`, or `run_id` — to mount. ### 1. Get a Mem0 API Key 1. Sign in to the [Mem0 dashboard](https://app.mem0.ai). 2. Open **API Keys**. 3. Create or copy a key (it starts with `m0-...`). The key is sent as a `Token` authorization header to the Mem0 API. Your organization and project are resolved from the key. ### 2. Choose a Scope Mem0 stores each memory under a single entity. A Mirage mount is scoped to exactly one of: | Scope | Mem0 filter | | ------------- | ----------- | | User | `user_id` | | Agent | `agent_id` | | Run / session | `run_id` | Set exactly one. Combining two (for example `user_id` **and** `agent_id`) is rejected, because a memory added with both is split into separate user-scoped and agent-scoped memories — a combined filter matches nothing. ### 3. Set Environment Variables ```bash theme={null} # .env.development MEM0_API_KEY=m0-... MEM0_USER_ID=alex ``` For an agent or run scope, set `MEM0_AGENT_ID` or `MEM0_RUN_ID` instead of `MEM0_USER_ID`. For self-hosted or a custom host, set `MEM0_HOST`: ```bash theme={null} MEM0_HOST=https://api.mem0.ai ``` For Python configuration, see the [Python Mem0 Setup](/python/setup/mem0) guide. # MinIO Source: https://docs.mirage.strukto.ai/home/setup/minio Set up MinIO credentials for the MinIO resource. ## Credentials ### 1. Create an access key 1. Open the **MinIO Console** (e.g. `http://localhost:9001`) and sign in 2. Go to **Access Keys** -> **Create access key** 3. Copy the **Access Key** and **Secret Key** 4. Note your server **endpoint** (e.g. `http://localhost:9000`) and **bucket** name MinIO is self-hosted, so there is no region-derived endpoint, the `endpoint_url` is required and MinIO uses path-style addressing. ### 2. Set environment variables ```bash theme={null} # .env.development MINIO_BUCKET=my-bucket MINIO_ENDPOINT=http://localhost:9000 MINIO_ACCESS_KEY=... MINIO_SECRET_KEY=... ``` For Python configuration, see the [MinIO resource](/python/resource/minio) docs. # MongoDB Source: https://docs.mirage.strukto.ai/home/setup/mongodb Set up a MongoDB connection for the MongoDB resource. ## Credentials ### 1. Get Your Connection URI #### Local MongoDB ```bash theme={null} # Default local instance MONGODB_URI=mongodb://localhost:27017 ``` #### MongoDB Atlas (Cloud) 1. Go to [https://cloud.mongodb.com](https://cloud.mongodb.com) 2. Select your cluster -> **Connect** -> **Drivers** 3. Copy the connection string: ``` mongodb+srv://:@cluster0.xxxxx.mongodb.net/ ``` #### Self-hosted with Authentication ```bash theme={null} MONGODB_URI=mongodb://username:password@host:27017/?authSource=admin ``` ### 2. Set Environment Variables ```bash theme={null} # .env.development MONGODB_URI=mongodb+srv://user:pass@cluster0.xxxxx.mongodb.net/ ``` For Python configuration, see the [Python MongoDB Setup](/python/setup/mongodb) guide. # Nextcloud Source: https://docs.mirage.strukto.ai/home/setup/nextcloud Set up Nextcloud / ownCloud / WebDAV credentials for the Nextcloud resource. ## Overview The Nextcloud resource talks to any [WebDAV](http://www.webdav.org/specs/rfc4918.html) server: self-hosted Nextcloud, ownCloud, Hetzner Storage Share, or any generic WebDAV endpoint. Authentication is HTTP Basic with an **app password** (not your account password). ## Credentials ### 1. Pick a server Any of these works: * **Nextcloud Hosted trial** at [https://try.nextcloud.com](https://try.nextcloud.com) * **Self-hosted**: `docker run -p 8080:80 nextcloud` * **Hetzner Storage Share**, **ownCloud**, or any WebDAV server ### 2. Create an app password In your Nextcloud web UI: 1. Open **Settings** → **Security** → **Devices & sessions** 2. Enter an app name (e.g. `mirage`) and click **Create new app password** 3. Copy the generated password (shown once) Login password also works but **app passwords are strongly preferred** so you can revoke per-app access without changing your account password. ### 3. Find your WebDAV URL The standard Nextcloud path is: ```text theme={null} https:///remote.php/dav/files// ``` For example: `https://cloud.example.com/remote.php/dav/files/alice/`. For generic WebDAV servers, use whatever URL the provider gives. ### 4. Set environment variables ```bash theme={null} # .env.development NEXTCLOUD_URL=https://cloud.example.com/remote.php/dav/files/alice/ NEXTCLOUD_USERNAME=alice NEXTCLOUD_PASSWORD= ``` For Python configuration, see the [Nextcloud resource](/python/resource/nextcloud) docs. # Notion Source: https://docs.mirage.strukto.ai/home/setup/notion Set up a Notion internal integration for the Notion resource. ## Credentials ### 1. Create an Internal Integration 1. Go to [My Integrations](https://www.notion.com/my-integrations) 2. Click **+ New integration** 3. Name it (e.g. "mirage"), select your workspace 4. Under **Content Capabilities**, enable: * **Read content** (required) * **Update content** (for write commands) * **Insert content** (for creating pages and comments) 5. Click **Save changes** 6. Copy the **Internal Integration Secret** (starts with `ntn_`) ### 2. Share Pages with the Integration The integration has no access by default. You must share each page: 1. Open a Notion page you want to mount 2. Click `...` (top-right) -> **+ Add connections** 3. Search for your integration name and select it 4. Child pages inherit the connection - share a top-level page to expose its entire subtree ### 3. Set Environment Variables ```bash theme={null} # .env.development NOTION_API_KEY=ntn_xxxx... ``` For Python configuration, see the [Python Notion Setup](/python/setup/notion) guide. # OCI Object Storage Source: https://docs.mirage.strukto.ai/home/setup/oci Set up Oracle Cloud Infrastructure Object Storage credentials for the OCI resource. ## Credentials The OCI resource needs: * an Object Storage bucket * your Object Storage namespace * your OCI region * a Customer Secret Key pair for the S3 Compatibility API ### 1. Create a Bucket 1. Open the OCI Console 2. Go to `Storage` -> `Buckets` 3. Choose the target compartment 4. Click `Create bucket` 5. Enter the bucket name, such as `mirage` 6. Keep the storage tier as `Standard` unless you specifically want archive If you want MIRAGE data under a prefix like `data/`, create objects under that prefix inside the bucket. The prefix is not part of the bucket name. ### 2. Get the Namespace 1. Open the profile menu in the OCI Console 2. Go to `Tenancy` 3. Copy the Object Storage namespace string, such as `idsiqbdbcr4i` You can also fetch it with: ```bash theme={null} oci os ns get ``` ### 3. Get S3-compatible Keys 1. Open the profile menu 2. Go to `User settings` 3. Open `Customer secret keys` 4. Click `Generate secret key` 5. Copy both the `Access Key` and `Secret Key` Oracle only shows the secret once, so save it immediately. ### 4. Find the Region Use the OCI Console region selector in the top-right. For example: * `us-ashburn-1` * `us-phoenix-1` * `eu-frankfurt-1` ### 5. Set Environment Variables ```bash theme={null} # .env.development OCI_BUCKET=mirage OCI_NAMESPACE=idsiqbdbcr4i OCI_REGION=us-ashburn-1 OCI_ACCESS_KEY_ID=... OCI_SECRET_ACCESS_KEY=... ``` For Python configuration, see the [OCI resource](/python/resource/oci) docs. # OneDrive Source: https://docs.mirage.strukto.ai/home/setup/onedrive Get a Microsoft Graph access token and drive ID for the OneDrive resource. The OneDrive resource talks to Microsoft OneDrive and SharePoint document libraries through the [Microsoft Graph API](https://learn.microsoft.com/en-us/graph/). It authenticates with an OAuth2 **bearer access token**, optionally scoped to a specific **drive**. ## Credentials You need two things: an **access token**, and (for app-only tokens or SharePoint) a **drive ID**. Pick whichever token path fits. ### Option A: Quick token (Graph Explorer) Fastest way to try it. The token lasts about an hour, which is plenty for a test run. 1. Open [Graph Explorer](https://developer.microsoft.com/graph/graph-explorer) and **Sign in**. 2. Run any query (for example `GET /me/drive`) and consent when prompted. 3. Open the **Access token** tab and copy the token. This is a **delegated** token, so it works against your own `/me/drive` and you do **not** need a drive ID. ### Option B: Repeatable token (app registration + device code) Use this for scripted or CI runs. 1. In the [Microsoft Entra admin center](https://entra.microsoft.com), go to **Identity** -> **Applications** -> **App registrations** -> **New registration**. For a personal account, choose *Accounts in any organizational directory and personal Microsoft accounts*. 2. Under **Authentication** -> **Advanced settings**, set **Allow public client flows** to **Yes**. 3. Under **API permissions** -> **Microsoft Graph** -> **Delegated**, add `Files.ReadWrite.All` (add `Sites.ReadWrite.All` for SharePoint libraries). 4. Run the device-code flow with your **Application (client) ID**: ```bash theme={null} CLIENT_ID="" # 1) request a device code curl -s -X POST "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode" \ -d "client_id=$CLIENT_ID" \ -d "scope=Files.ReadWrite.All offline_access" # -> open https://microsoft.com/devicelogin and enter the user_code it prints # 2) after you sign in, exchange the device_code for a token curl -s -X POST "https://login.microsoftonline.com/common/oauth2/v2.0/token" \ -d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \ -d "client_id=$CLIENT_ID" \ -d "device_code=" | jq -r .access_token ``` **App-only tokens** (client-credentials flow) have no signed-in user, so `/me/drive` returns an error. With an app-only token you **must** pass a `drive_id` (or `site_id`). Delegated tokens (Options A and B) can use `/me/drive` with no drive ID. ### Get your drive ID Only needed for app-only tokens or to target a specific SharePoint library. With your token exported as `TOKEN`: ```bash theme={null} # your personal OneDrive drive id curl -s -H "Authorization: Bearer $TOKEN" \ https://graph.microsoft.com/v1.0/me/drive | jq -r .id # list every drive you can see (name + id) curl -s -H "Authorization: Bearer $TOKEN" \ https://graph.microsoft.com/v1.0/me/drives | jq '.value[] | {name, id}' # a SharePoint site's drives curl -s -H "Authorization: Bearer $TOKEN" \ "https://graph.microsoft.com/v1.0/sites/{hostname}:/sites/{site-path}" | jq -r .id curl -s -H "Authorization: Bearer $TOKEN" \ "https://graph.microsoft.com/v1.0/sites/{site-id}/drives" | jq '.value[] | {name, id}' ``` The drive ID looks like `b!a1B2c3...`. ### Set environment variables ```bash theme={null} # .env.development ONEDRIVE_ACCESS_TOKEN= ONEDRIVE_DRIVE_ID= # omit when using a delegated /me/drive token ``` Snapshots and version pinning rely on OneDrive/SharePoint **version history**, which is on by default. Older versions are only readable while the library retains them (the version cap is configurable, and an admin can disable versioning). For Python configuration, see the [Python OneDrive Setup](/python/setup/onedrive) guide. # Postgres Source: https://docs.mirage.strukto.ai/home/setup/postgres Set up a Postgres connection for the Postgres resource. ## Credentials ### 1. Get Your Connection DSN #### Local Postgres ```bash theme={null} # Default local instance DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres ``` #### Managed Postgres (Supabase, Neon, RDS, ...) Copy the connection string from your provider's dashboard. It usually looks like: ``` postgres://:@:5432/?sslmode=require ``` ### 2. Set Environment Variables ```bash theme={null} # .env.development DATABASE_URL=postgres://user:pass@host:5432/db?sslmode=require ``` ## Permissions The Postgres resource is read-only. Grant the role you connect with at minimum: ```sql theme={null} GRANT USAGE ON SCHEMA public TO mirage_reader; GRANT SELECT ON ALL TABLES IN SCHEMA public TO mirage_reader; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO mirage_reader; ``` If you only want to expose a subset of schemas, set `schemas` in the resource config: ```python theme={null} PostgresConfig(dsn=os.environ["DATABASE_URL"], schemas=["public", "analytics"]) ``` ## Limits The resource has built-in safety limits to keep an agent from accidentally pulling a whole table: * `default_row_limit` (1,000): default LIMIT applied to ad-hoc reads. * `max_read_rows` (10,000): hard ceiling per read. * `max_read_bytes` (10 MiB): hard ceiling per read. * `default_search_limit` (100): default LIMIT for search-style queries. Override in the config if you need bigger reads. # Qdrant Source: https://docs.mirage.strukto.ai/home/setup/qdrant Set up a Qdrant connection for the Qdrant resource. [Qdrant](https://qdrant.tech/) mounts a collection as a read-only filesystem: group-by payload fields become nested folders, each point is a `.json` payload file (plus a `.txt` text file and an optional blob), and semantic search is the `search` command. ## Connection ### Local / self-hosted Point `host`/`port` at a running Qdrant (defaults `localhost:6333`): ```bash theme={null} # .env.development QDRANT_HOST=localhost QDRANT_PORT=6333 ``` ### Qdrant Cloud Use `url` plus an `api_key`: ```bash theme={null} # .env.development QDRANT_URL=https://xyz.us-east4-0.gcp.cloud.qdrant.io QDRANT_API_KEY=... ``` ## Search Search is the `search "" ` command. It returns ranked points as their canonical `.txt` (or `.json`) file paths plus a similarity score, so results compose with `cat`, `wc`, and pipes (`grep`/`rg` stay lexical). The query text is turned into a vector two ways: * **Local (default):** the `qdrant-client[fastembed]` extra embeds the query in process with `embedding_model` (default `sentence-transformers/all-MiniLM-L6-v2`). * **Server-side:** set `cloud_inference` to let a Qdrant Cloud (inference-enabled) cluster embed the query. The TypeScript backend always uses this path. Either way the collection must already store vectors produced by the same model. ## Limits The resource is read-only and bounds how much an agent can pull: * `search_limit` (10): default top-k returned by `search`. * `max_rows` (1,000): hard ceiling on points listed per folder. Folder listings filter on payload fields. A filtered listing scrolls first and only creates keyword payload indexes for the `group_by` fields if Qdrant reports one is required. `max_rows` caps how many points are scanned per folder. See the [Python](/python/resource/qdrant) and [TypeScript](/typescript/setup/qdrant) resource pages for full config. # QingStor Source: https://docs.mirage.strukto.ai/home/setup/qingstor Set up QingStor (QingCloud) Object Storage credentials for the QingStor resource. ## Credentials ### 1. Create an access key 1. Sign in to the [QingCloud console](https://console.qingcloud.com/) 2. Go to **API Keys** (under your account) -> **Create** 3. Copy the **Access Key ID** and **Secret Access Key** 4. Note your bucket's **zone** (e.g. `pek3a`) and **bucket** name The endpoint is derived from the zone (`region`) as `s3..qingstor.com`, QingStor's S3-compatible host (distinct from the native `.qingstor.com`). Set `QINGSTOR_ENDPOINT_URL` to override. ### 2. Set environment variables ```bash theme={null} # .env.development QINGSTOR_BUCKET=my-bucket QINGSTOR_ZONE=pek3a QINGSTOR_ACCESS_KEY_ID=... QINGSTOR_SECRET_ACCESS_KEY=... # QINGSTOR_ENDPOINT_URL=https://s3.pek3a.qingstor.com # optional override ``` For Python configuration, see the [QingStor resource](/python/resource/qingstor) docs. # Cloudflare R2 Source: https://docs.mirage.strukto.ai/home/setup/r2 Set up Cloudflare R2 credentials for the R2 resource. ## Credentials ### 1. Create R2 API Token 1. Go to [https://dash.cloudflare.com/](https://dash.cloudflare.com/) -> **R2 Object Storage** 2. **Manage R2 API Tokens** -> **Create API Token** 3. Set permissions: **Object Read** (or **Object Read & Write**) 4. Scope to your bucket 5. Copy the **Access Key ID** and **Secret Access Key** 6. Note your **Account ID** from the Cloudflare dashboard URL ### 2. Set Environment Variables ```bash theme={null} # .env.development R2_BUCKET=my-bucket R2_ACCOUNT_ID=abc123... R2_ACCESS_KEY_ID=... R2_SECRET_ACCESS_KEY=... ``` For Python configuration, see the [R2 resource](/python/resource/r2) docs. # S3 Source: https://docs.mirage.strukto.ai/home/setup/s3 Set up AWS S3 credentials for the S3 resource. ## Credentials The S3 resource needs an AWS access key pair with read (and optionally write) access to your bucket. ### 1. Create an IAM User 1. Go to [https://console.aws.amazon.com/iam/](https://console.aws.amazon.com/iam/) 2. **Users** -> **Create user** 3. Attach the `AmazonS3ReadOnlyAccess` policy (or `AmazonS3FullAccess` for write) 4. **Security credentials** -> **Create access key** -> **Application running outside AWS** 5. Copy the **Access key ID** and **Secret access key** ### 2. Set Environment Variables ```bash theme={null} # .env.development AWS_S3_BUCKET=my-bucket AWS_DEFAULT_REGION=us-east-1 AWS_ACCESS_KEY_ID=AKIA... AWS_SECRET_ACCESS_KEY=wJal... ``` ### Alternative: AWS Profile If you have `~/.aws/credentials` configured, you can use a profile name instead of explicit access keys. For the Python resource API, see the [S3 resource doc](/python/resource/s3). ## Scoping a resource to a key prefix Both runtimes support a key prefix option that transparently scopes every operation to a subpath of the bucket, so agents see clean paths while the underlying S3 keys carry the full prefix. ```ts TypeScript theme={null} const s3 = new S3Resource({ bucket: 'app-data', region: 'eu-west-1', keyPrefix: `users/${userId}/`, }) ``` ```python Python theme={null} S3Resource(S3Config( bucket="app-data", region="eu-west-1", key_prefix=f"users/{user_id}/", )) ``` Leading slashes are stripped and a trailing slash is added automatically. `None` / `undefined` and empty strings both mean "no prefix." For browser security considerations, see the [TypeScript S3 setup doc](/typescript/setup/s3). # Scaleway Source: https://docs.mirage.strukto.ai/home/setup/scaleway Set up Scaleway Object Storage credentials for the Scaleway resource. ## Credentials ### 1. Create an API key 1. Sign in to the [Scaleway console](https://console.scaleway.com/) 2. Go to **IAM** -> **API Keys** -> **Generate API Key** 3. Copy the **Access Key** and **Secret Key** 4. Note your bucket's **region** (e.g. `fr-par`) and **bucket** name The endpoint is derived from `region` as `s3..scw.cloud`. ### 2. Set environment variables ```bash theme={null} # .env.development SCW_BUCKET=my-bucket SCW_REGION=fr-par SCW_ACCESS_KEY=... SCW_SECRET_KEY=... ``` For Python configuration, see the [Scaleway resource](/python/resource/scaleway) docs. # SeaweedFS Source: https://docs.mirage.strukto.ai/home/setup/seaweedfs Set up SeaweedFS credentials for the SeaweedFS resource. ## Credentials SeaweedFS serves an S3-compatible gateway (the `weed s3` command, default port `8333`). Access keys are defined in the gateway's S3 config (`-s3.config`), or the gateway can run without authentication for local development. ### 1. Get an access key 1. Start the S3 gateway, e.g. `weed server -s3` or `weed s3 -filer=localhost:8888` 2. If you configured `-s3.config`, copy an identity's **accessKey** and **secretKey** 3. Note your gateway **endpoint** (e.g. `http://localhost:8333`) and **bucket** name SeaweedFS uses path-style addressing, so the `endpoint_url` is required. ### 2. Set environment variables ```bash theme={null} # .env.development SEAWEEDFS_BUCKET=my-bucket SEAWEEDFS_ENDPOINT=http://localhost:8333 SEAWEEDFS_ACCESS_KEY=... SEAWEEDFS_SECRET_KEY=... ``` Blaxel Agent Drive is backed by SeaweedFS. Use the drive's `s3Url` as the endpoint and a drive access token's credentials to mount it as a SeaweedFS resource. For Python configuration, see the [SeaweedFS resource](/python/resource/seaweedfs) docs. # SharePoint Source: https://docs.mirage.strukto.ai/home/setup/sharepoint Get a Microsoft Graph access token for the SharePoint resource. The SharePoint resource talks to Microsoft SharePoint Online through the [Microsoft Graph API](https://learn.microsoft.com/en-us/graph/). It authenticates with an OAuth2 **bearer access token**. Unlike OneDrive, you do **not** need a drive id: the resource discovers sites and document libraries for you, so a token with the right SharePoint permissions is all it needs. ## Credentials You only need an **access token**. Pick whichever path fits. ### Option A: App-only (client credentials) Best for scripted, automated, and CI runs. There is no signed-in user, and it works for SharePoint because the resource uses `/sites` and `/sites/{id}/drives` (no `/me` context required). 1. In the [Microsoft Entra admin center](https://entra.microsoft.com), go to **Identity** -> **Applications** -> **App registrations** -> **New registration**. 2. Under **API permissions** -> **Microsoft Graph** -> **Application permissions**, add `Sites.Read.All` for read-only, or `Sites.ReadWrite.All` (plus `Files.ReadWrite.All` for writes) for full access. Click **Grant admin consent**. 3. Under **Certificates & secrets**, create a **client secret**. 4. Request a token with the client-credentials flow: ```bash theme={null} TENANT_ID="" CLIENT_ID="" CLIENT_SECRET="" curl -s -X POST "https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/token" \ -d "client_id=$CLIENT_ID" \ -d "client_secret=$CLIENT_SECRET" \ -d "scope=https://graph.microsoft.com/.default" \ -d "grant_type=client_credentials" | jq -r .access_token ``` ### Option B: Delegated (Graph Explorer) Fastest way to try it as yourself. The token lasts about an hour, which is plenty for a test run. 1. Open [Graph Explorer](https://developer.microsoft.com/graph/graph-explorer) and **Sign in**. 2. Run any SharePoint query (for example `GET /sites?search=*`) and consent when prompted. 3. Open the **Access token** tab and copy the token. This is a **delegated** token scoped to your own SharePoint access. A static token is short-lived (about 60 minutes). For long-running mounts, pass a `Callable[[], str]` provider as `access_token` instead of a string. The resource refreshes on `401`, so the mount survives token expiry. ### Verify access With your token exported as `TOKEN`: ```bash theme={null} # list sites you can see (name + id) curl -s -H "Authorization: Bearer $TOKEN" \ "https://graph.microsoft.com/v1.0/sites?search=*" | jq '.value[] | {displayName, id}' # list the document libraries (drives) in a site curl -s -H "Authorization: Bearer $TOKEN" \ "https://graph.microsoft.com/v1.0/sites/{site-id}/drives" | jq '.value[] | {name, id}' ``` ### Set environment variables ```bash theme={null} # .env.development MS_GRAPH_DRIVE_TOKEN= ``` Snapshots and version pinning rely on SharePoint **version history**, which is on by default. Older versions are only readable while the library retains them (the version cap is configurable, and an admin can disable versioning). For Python configuration, see the [Python SharePoint resource](/python/resource/sharepoint) guide. # Slack Source: https://docs.mirage.strukto.ai/home/setup/slack Set up a Slack Bot Token for the Slack resource. ## Credentials ### 1. Create a Slack App 1. Go to [https://api.slack.com/apps](https://api.slack.com/apps) 2. **Create New App** -> **From scratch** 3. Name it (e.g., "Mirage") and select your workspace ### 2. Configure Bot Permissions 1. **OAuth & Permissions** in the left sidebar 2. Under **Bot Token Scopes**, add: * `channels:history` - read messages in public channels * `channels:read` - list public channels * `groups:history` - read messages in private channels (optional) * `groups:read` - list private channels (optional) * `chat:write` - send messages (for write mode) ### 3. Install to Workspace 1. **Install App** in the left sidebar -> **Install to Workspace** 2. Authorize the requested permissions 3. Copy the **Bot User OAuth Token** (`xoxb-...`) ### 4. Optional: Enable Workspace Search Slack's `search.messages` API does not accept bot tokens. To use `slack search` or workspace-level search push-down: 1. Add `search:read` under **User Token Scopes**. 2. Reinstall or reauthorize the app. 3. Copy the resulting **User OAuth Token** (`xoxp-...`). Keep using the bot token for normal reads and writes. Mirage uses the optional user token only for Slack search endpoints. ### 5. Set Environment Variables ```bash theme={null} # .env.development SLACK_BOT_TOKEN=xoxb-xxxx... SLACK_USER_TOKEN=xoxp-xxxx... # optional, only needed for workspace search ``` ### 6. Invite the Bot The bot must be invited to channels it needs to read: ``` /invite @Mirage ``` For Python configuration, see the [Python Slack Setup](/python/setup/slack) guide. # Supabase Storage Source: https://docs.mirage.strukto.ai/home/setup/supabase Set up Supabase Storage credentials for the Supabase resource. ## Credentials Supabase Storage exposes an S3-compatible API at `https://.storage.supabase.co/storage/v1/s3`, signed with **S3 Access Keys** scoped to a Supabase project (not your `anon` or `service_role` JWT). ### 1. Create an S3 access key 1. Open the [Supabase dashboard](https://supabase.com/dashboard) and pick your project. 2. **Storage** -> **Settings** -> **S3 Access Keys**. 3. **New access key** -> scope to the bucket(s) you want Mirage to mount. 4. Copy the **Access key ID** and **Secret access key**, the secret is shown once. ### 2. Note the project reference and region * **Project reference** is in the dashboard URL: `https://supabase.com/dashboard/project/`. * **Region** is the region you selected when creating the project. Supabase requires a region string for SigV4 signing even though it is metadata only. ### 3. Set Environment Variables ```bash theme={null} # .env.development SUPABASE_BUCKET=my-bucket SUPABASE_REGION=us-east-1 SUPABASE_PROJECT_REF=abcdefghijklmnop SUPABASE_ACCESS_KEY_ID=... SUPABASE_SECRET_ACCESS_KEY=... ``` Supabase requires **path-style** S3 URLs. The Mirage resource sets `forcePathStyle: true` automatically when you pass `project_ref`. For Python configuration, see the [Python Supabase Resource](/python/resource/supabase) guide. For Node and browser wiring, see the [TypeScript Supabase Setup](/typescript/setup/supabase) guide. # Tencent COS Source: https://docs.mirage.strukto.ai/home/setup/tencent Set up Tencent Cloud Object Storage (COS) credentials for the Tencent resource. ## Credentials ### 1. Create an API key 1. Sign in to the [Tencent Cloud console](https://console.cloud.tencent.com/) 2. Go to **Access Management** -> **API Keys** -> **Create Key** 3. Copy the **SecretId** (access key id) and **SecretKey** (secret access key) 4. Note your bucket's **region** (e.g. `ap-guangzhou`) and the full **bucket** name including the APPID suffix (e.g. `my-bucket-1250000000`) The endpoint is derived from `region` as `cos..myqcloud.com`. Tencent COS bucket names always include the APPID suffix. Use the full name (with the suffix) for `COS_BUCKET`. ### 2. Set environment variables ```bash theme={null} # .env.development COS_BUCKET=my-bucket-1250000000 COS_REGION=ap-guangzhou COS_SECRET_ID=... COS_SECRET_KEY=... ``` For Python configuration, see the [Tencent resource](/python/resource/tencent) docs. # Trello Source: https://docs.mirage.strukto.ai/home/setup/trello Set up Trello API credentials for the Trello resource. ## Credentials ### 1. Get an API Key 1. Go to [https://trello.com/power-ups/admin](https://trello.com/power-ups/admin) 2. Create a new Power-Up (or use an existing one) 3. Copy the **API Key** from the Power-Up's API Key page ### 2. Generate a Token Open the following URL in your browser, replacing `YOUR_API_KEY`: ``` https://trello.com/1/authorize?expiration=never&scope=read,write&response_type=token&key=YOUR_API_KEY ``` Click **Allow** and copy the token string. ### 3. Set Environment Variables ```bash theme={null} # .env.development TRELLO_API_KEY=your-api-key TRELLO_API_TOKEN=your-api-token ``` For Python configuration, see the [Python Trello Setup](/python/setup/trello) guide. # Wasabi Source: https://docs.mirage.strukto.ai/home/setup/wasabi Set up Wasabi credentials for the Wasabi resource. ## Credentials ### 1. Create an access key 1. Sign in to the [Wasabi console](https://console.wasabisys.com/) 2. Go to **Access Keys** -> **Create New Access Key** 3. Copy the **Access Key** and **Secret Key** 4. Note your bucket's **region** (e.g. `us-east-2`) and **bucket** name The endpoint is derived from `region` as `s3..wasabisys.com` (`s3.wasabisys.com` for `us-east-1`). ### 2. Set environment variables ```bash theme={null} # .env.development WASABI_BUCKET=my-bucket WASABI_REGION=us-east-2 WASABI_ACCESS_KEY_ID=... WASABI_SECRET_ACCESS_KEY=... ``` For Python configuration, see the [Wasabi resource](/python/resource/wasabi) docs. # Windows Source: https://docs.mirage.strukto.ai/home/setup/windows Set up WinFsp on Windows for MIRAGE FUSE mounts (Python, experimental). ## FUSE on Windows Windows has no native FUSE. MIRAGE's Python FUSE mounts run on [WinFsp](https://winfsp.dev/), a maintained Windows file system driver with a FUSE-compatible layer. Support is **experimental**: the FUSE integration battery passes in CI on `windows-latest`, but Windows is not yet a fully supported platform. The TypeScript SDK has no Windows FUSE support at all (its binding only targets macOS and Linux). ### Install WinFsp Requires Windows 10 (1809+) or Windows 11. ```powershell theme={null} winget install -e --id WinFsp.WinFsp ``` ```powershell theme={null} choco install winfsp -y ``` Download the MSI from the [WinFsp releases page](https://winfsp.dev/rel/) and run it. No reboot or driver-signing steps are needed (unlike macFUSE on macOS). ### Verify ```powershell theme={null} pip install "mirage-ai[fuse]" python -c "import mfusepy; print('WinFsp FUSE loaded')" ``` `mfusepy` locates `winfsp-x64.dll` through the registry; if the import succeeds, the driver is reachable. ## Windows-specific behavior MIRAGE handles the WinFsp conventions automatically, but three behaviors differ from macOS/Linux and are worth knowing: * **Unmount happens at process exit.** There is no `fusermount` on Windows; WinFsp releases the mount when the process serving it exits. Closing the workspace does not actively unmount. * **Ownership is a mount-level mapping.** All files appear owned by the user who mounted the filesystem (WinFsp's `uid=-1,gid=-1` mapping). Per-file POSIX owners from backends are not represented. * **`stat` opens a handle.** Windows cannot query file attributes without opening the file, so size-unknown API-backed files fetch their content on the first per-file `stat` and immediately report the real size — where macOS/Linux report 0 until first open. Directory listings stay cheap on all platforms. Unlike macOS, multiple FUSE mounts per process work on Windows. See the [FUSE support matrix](/home/setup/fuse) for the full OS and language overview. # Snapshot & Replay Source: https://docs.mirage.strukto.ai/home/snapshot Freeze an agent run, restore it later, and detect when the underlying world has drifted. ## What It Does A snapshot captures a workspace as a single tar file: mount configs, sessions, history, finished jobs, cache bytes, and one fingerprint per recorded remote read. Loading a snapshot reconstructs the workspace and verifies that the underlying sources have not drifted since capture. When the backend exposes a stable per-object revision marker (S3 `VersionId`, Drive `revisionId`, Git commit SHA), the snapshot also records that revision. At load time, reads pin to the recorded revision and serve the exact bytes the original agent saw, even if the live object has since been overwritten. ```mermaid theme={null} flowchart TD Load["Workspace.load(tar)"] --> Inject{Manifest carries
revision for path?} Inject -->|yes| Pin["mount.revisions[path] = revision"] Inject -->|no| Mark[Queue (mount, path, fingerprint)
for drift check] Pin --> Ready[Workspace ready] Mark --> Ready Ready --> First[First dispatch / execute] First --> Gather[asyncio.gather drift checks
pinned paths never queued] Gather --> Read[Read at path] Read --> Pinned{revision_for path
set?} Pinned -->|yes| Versioned[GET path with VersionId
= original bytes] Pinned -->|no| Live[GET path live] Live --> Match{Fingerprint
matches snapshot?} Match -->|yes| Serve[Serve current bytes] Match -->|"no, STRICT"| Raise[raise ContentDriftError] Match -->|"no, OFF"| Evict[Evict cache, serve live] ``` ## The API ```python Python theme={null} # capture — async; serializes state plus fingerprints recorded during reads await ws.snapshot("run.tar") await ws.snapshot("run.tar.gz", compress="gz") # replay — drift check fires on first dispatch/execute restored = await Workspace.load("run.tar") # STRICT default restored = await Workspace.load("run.tar", drift_policy=DriftPolicy.OFF) # in-process duplicate (shares remote resources, restores local content fresh) cp = await ws.copy() ``` ```typescript TypeScript theme={null} import { DriftPolicy, Workspace } from '@struktoai/mirage-node' // capture to a tar file await ws.snapshot('run.tar') // replay with strict drift detection (default) const restored = await Workspace.load('run.tar') // or restore the structure while reading current remote content const current = await Workspace.load('run.tar', { driftPolicy: DriftPolicy.OFF, }) // in-process duplicate const copy = await ws.copy() ``` TypeScript file-path snapshot I/O is available in Node. In the browser, `Workspace.load(snapshotBytes)` accepts a `Uint8Array`, and `Workspace.fromState(...)` restores a state object; writing the tar to a file or download target is the application's responsibility. Both `snapshot` and `copy` are async because they serialize workspace state and collect recorded fingerprints. ## Versioning: commit, checkout, clone A tar snapshot is a *portable* capture you move between machines. Versioning is the in-place complement: a git-style history kept *with* the workspace, server-side and keyed by workspace id, so you can checkpoint, roll back, branch, and fork without writing a file. It is backed by a real git object store (dulwich) inside the daemon. This is a CLI / REST feature, not an SDK method (there is no `ws.commit()` in-process): ```bash theme={null} mirage workspace commit demo -m "before refactor" # checkpoint the live state mirage workspace checkout demo # roll back in place mirage workspace clone demo --at # fork a past version ``` See the [CLI versioning reference](/home/cli#versioning) for `log`, `diff`, `branch`, and the full flag set. | | Tar snapshot | Versioning | | -------- | ------------------------------------------ | ------------------------------------------------------------- | | Output | a `.tar` you move or archive | history kept with the workspace (server) | | API | `ws.snapshot()` / `Workspace.load()` (SDK) | `mirage workspace commit` / `checkout` / `clone` (CLI + REST) | | Lineage | none, each tar stands alone | git-style commits, branches, diff | | Best for | reproducing a run on another machine | iterating in place with checkpoints to revert to | ## What Is And Isn't Captured * Mount configs (creds redacted; restore via `resources=` override) * Sessions, history, finished jobs * Cache bytes for touched paths * One fingerprint per remote read (ETag-equivalent) * Optional per-path `revision` when the backend exposes one * Live state of mounts with `SUPPORTS_SNAPSHOT=False` (Gmail, Slack, Linear, Notion, …) * Files the agent never touched * Raw bytes of remote objects (recoverable only via revision pin) ## Drift Detection On the first `dispatch` or `execute` after `load`, Mirage stats every fingerprinted path against the live source in parallel. If any path's live fingerprint differs from the recorded one, the workspace raises `ContentDriftError`: ```python theme={null} try: await ws.execute("cat /s3/data.csv") except ContentDriftError as exc: print(exc.path, exc.snapshot_fingerprint, exc.live_fingerprint) ``` Paths that carry a revision pin are skipped — the pinned read serves the exact original bytes, so a fingerprint mismatch is expected and harmless. ## Drift Policies | Policy | Behavior on mismatch | Use when | | -------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------- | | `STRICT` *(default)* | Raise `ContentDriftError` on first mismatch. | Reproducing an agent run; you want to know the world moved. | | `OFF` | Skip drift checks entirely. Evict snapshot cache for fingerprinted paths so reads serve current. | You only wanted the workspace skeleton, not the bytes. | Pass via `await Workspace.load(..., drift_policy=DriftPolicy.OFF)`. ## How It Composes With Caching Snapshots interact with two existing caches: | Cache | Holds | Snapshotted? | | ----------------------------------- | ------------------------------- | ----------------------------------------------------- | | **File cache** (`Workspace._cache`) | raw bytes per virtual path | ✅ bytes for touched paths are serialized into the tar | | **Index cache** (`Resource._index`) | `FileStat` entries for listings | ❌ rebuilt lazily after load | **Only files the agent actually read are fingerprinted.** Capture walks `ws._ops.records` for `op == "read"` and dedups by path. Files that were listed but never opened, or touched only by `stat` / `readdir`, do not carry a fingerprint or a revision. At load time, three pieces of restored state cooperate per read: 1. **Cache is consulted first.** Snapshot bytes go back into `Workspace._cache`; a warm read returns them with no network round-trip. 2. **Fingerprint verifies the cache.** Under STRICT, the eager drift check stats every recorded path against live and raises before any read fires if anything moved. The cache is therefore trusted as authoritative until proven stale. 3. **Revision pin is the cold-path recovery.** When the cache misses and the backend supports pinning, reads fetch the exact recorded revision (S3 `GetObject(VersionId=...)`) so you still get original bytes, not the live head. The same three states under each policy: | Scenario | Cache | Pin | Drift check | Net effect | | --------------------------------------- | ------------------------------- | ------------- | -------------------------- | --------------------------------------- | | STRICT, fingerprint matches, cache warm | served | n/a | passes | Original bytes from cache (\~0 ms) | | STRICT, fingerprint matches, cache cold | miss | n/a | passes | Live GET, current bytes (= original) | | STRICT, no pin, fingerprint differs | (n/a) | (n/a) | raises `ContentDriftError` | Caller informed before reading | | STRICT + pin (versioned backend) | served (matches recorded) | installed | skipped for pinned paths | Original bytes from cache or pinned GET | | OFF, any state | evicted for fingerprinted paths | not installed | skipped | Live GET, current bytes | The cache is the optimization, the fingerprint is the verifier, and the pin is the recovery — three independent guarantees that "what you replay equals what you captured." ## Resource Support Matrix Remote drift detection is opt-in per resource through `SUPPORTS_SNAPSHOT` in Python and `supportsSnapshot` in TypeScript. A working adapter must also attach a fingerprint to each read record; a revision is optional and enables pinned replay. Legend: ✅ = implemented · 🟡 = resource opts in, but recorded reads do not yet carry the fingerprint · ❌ = live-only · — = unavailable in that runtime. ### Remote Resources | Resource family | Python | TS Node | TS Browser | Revision pin | Notes | | ---------------------------------- | :----: | :-----: | :--------: | :---------------------------: | ------------------------------------------------------------------------------------ | | S3 and S3-compatible object stores | ✅ | ✅ | ✅ | When `VersionId` is available | Uses `ETag`; compatible providers without object versions still get drift detection. | | OneDrive | ✅ | — | — | ✅ | Uses Microsoft Graph `cTag` plus the current version id. | | SharePoint | ✅ | — | — | ✅ | Same Microsoft Graph version flow as OneDrive. | | Hugging Face resources | 🟡 | 🟡 | — | ❌ | Resources opt in, but read records do not yet include the stat fingerprint. | | Nextcloud | 🟡 | — | — | ❌ | The resource opts in, but read records do not yet include the WebDAV ETag. | | GitHub and Google Drive | ❌ | ❌ | ❌ | ❌ | Current reads are live-only; revision-aware replay is not wired yet. | | Other remote resources | ❌ | ❌ | ❌ | ❌ | Snapshot restores their config/state, then reads current remote content. | ### Local State RAM, Disk, and Redis serialize their resource state into the snapshot. They do not need a remote drift check or revision pin: replay restores the captured state directly. Credentials and connection details remain redacted and may require a resource override at load time. ## Extending To A New Backend Three steps in Python: ```python theme={null} from mirage.observe.context import record, revision_for from mirage.resource.base import BaseResource from mirage.types import FileStat class MyResource(BaseResource): SUPPORTS_SNAPSHOT = True # 1. opt in async def stat(self, ...) -> FileStat: return FileStat( ..., fingerprint=my_etag_equivalent, # 2. for drift revision=my_revision_marker_or_None, # 3. optional, for pin ) ``` And in your read function, look up the active pin and pass both the fingerprint and the revision through to `record` so the snapshot captures whatever the backend served: ```python theme={null} async def read_bytes(accessor, virtual_path, ...): pinned = revision_for(virtual_path) # 4. None if no pin installed response = backend_get(virtual_path, revision=pinned) record( "read", virtual_path, "my-backend", len(response.body), start_ms, fingerprint=response.etag, # 5. for drift detection revision=response.revision, # 6. for pinning on replay ) return response.body ``` At load time the workspace writes each manifest entry straight into `mount.revisions` — no per-resource hook required. If your backend has no stable revision, skip steps 3 and 6; drift detection still works on the fingerprint alone. ## Caveats * **Revision longevity.** Pinned reads only work as long as the source still retains the recorded revision. S3 bucket lifecycle rules can age out old versions; Drive keeps revisions for 30 days on non-Workspace files. Treat pins as best-effort. * **First read after load is slower than the rest.** `Workspace.load()` returns immediately, but the first `execute()` or `dispatch()` afterwards pauses while Mirage verifies that nothing has drifted upstream. Concretely, it asks the live source "is this path still the bytes I remember?" once per recorded read, in parallel. Tiny for short sessions (tens of milliseconds); a few hundred milliseconds to a couple of seconds for sessions with hundreds of recorded reads. The pause happens once per loaded workspace; subsequent calls are normal speed. Pass `drift_policy=DriftPolicy.OFF` if you want to skip the check entirely. # Troubleshooting Source: https://docs.mirage.strukto.ai/home/troubleshooting Common Mirage setup failures and the fastest path to getting unblocked. ## Common Issues ### Import Errors After Install (Python) * Make sure you installed the package into the environment you are actually using. * If you need resource-specific dependencies, install the matching extra such as `mirage-ai[s3]`, `mirage-ai[redis]`, or `mirage-ai[fuse]`. * For source installs, run `uv sync --all-extras --no-extra camel` (the `camel` extra conflicts with the `openai` stack, so excluding it keeps the rest installable). ### Import Errors After Install (TypeScript) * Pick the entrypoint that matches your runtime: `mirage/node` for Node servers and CLIs, `mirage/browser` for browser and edge runtimes, `mirage/core` for runtime-agnostic primitives. * Native peers like FUSE and Redis are opt-in. Install them only on Node, alongside `mirage`, when you need those resources. * If your bundler complains about Node built-ins, you are probably importing `mirage/node` from a browser entry. Switch to `mirage/browser` or `mirage/core`. ### Credential Errors * Check that the resource-specific environment variables are set in the current shell. * Confirm you followed the right setup guide from the [Setup](/home/setup/s3) section. * If a resource depends on Google OAuth or bot tokens, verify the scopes and app installation steps. ### FUSE Does Not Mount * Follow the OS-level setup first: [macOS](/home/setup/macos) or [Linux](/home/setup/linux). * Then confirm you installed `mirage[fuse]`. * If Mirage falls back to virtual mode, check the FUSE installation before debugging command behavior. ### Commands Return Empty Output * Verify the mount prefix and the path you are querying. * Start with `ls`, `tree`, or `stat` before assuming the resource is broken. * For remote systems, confirm the underlying account has access to the target resource. ### Docs and Examples Disagree * Prefer the Python quickstart and the current resource docs over older snippets copied from issues or experiments. * If something still looks inconsistent, open an issue or contact the team directly. ## Book Time With The Team If you are blocked and want direct help, book a troubleshooting call: Book time with the Mirage team on Cal.com. # Agno Source: https://docs.mirage.strukto.ai/python/agents/agno Give Agno agents shell-based filesystem access to any Mirage workspace using MirageToolkit. [Agno](https://github.com/agno-agi/agno) is an agent framework that groups related tools into `Toolkit` classes. Mirage ships `MirageToolkit`, a toolkit backed by a `Workspace` instead of the host machine. The agent gets five shell-style tools (`execute`, `read`, `write`, `ls`, `grep`); mount RAM, S3, GDrive, Slack, etc. and the agent uses them through the same Agno API. ## Install ```bash theme={null} uv add 'mirage-ai[agno]' ``` Requires `agno>=2.7.4`. The toolkit registers both sync and async variants through Agno's `tools` and `async_tools` parameters. ## Usage ```python theme={null} import asyncio from agno.agent import Agent from agno.models.openai import OpenAIChat from mirage import MountMode, RAMResource, Workspace from mirage.agents.agno import MirageToolkit ws = Workspace({"/data": RAMResource()}, mode=MountMode.WRITE) agent = Agent( model=OpenAIChat(id="gpt-5.4-mini"), tools=[MirageToolkit(ws)], instructions=("You have access to a virtual filesystem via shell " "tools. Use them to explore and read files."), markdown=True, ) async def main() -> None: await ws.execute('echo "hello from mirage" | tee /data/hello.txt') await agent.aprint_response( "List all files under /data and show the contents of each one.") asyncio.run(main()) ``` ## Tools Every tool is registered as a sync + async pair under one name; Agno picks the async variant in `arun`/`aprint_response` and the sync variant otherwise. | Tool | Mirage translation | | ---------------------- | ---------------------------------------------------- | | `execute(command)` | `await ws.execute(command)`, full shell with pipes | | `read(path)` | `cat ` | | `write(path, content)` | `mkdir -p `, then `tee ` with `stdin=` | | `ls(path="/")` | `ls ` | | `grep(pattern, path)` | `grep -r ` | ## Exports | Symbol | Purpose | | ---------------------- | ------------------------------------------------------------------------- | | `MirageToolkit` | Agno `Toolkit` exposing the 5 shell tools, backed by `Workspace.execute`. | | `MIRAGE_SYSTEM_PROMPT` | Default system prompt describing the virtual filesystem. | | `build_system_prompt` | Compose the default prompt with mount info and extra instructions. | ## Examples * [`examples/python/agents/agno/agno_example.py`](https://github.com/strukto-ai/mirage/blob/main/examples/python/agents/agno/agno_example.py), `Agent` with `MirageToolkit` over a RAM workspace, sync and async. # CAMEL-AI Source: https://docs.mirage.strukto.ai/python/agents/camel Run CAMEL-AI ChatAgents against a Mirage workspace using MirageTerminalToolkit and MirageFileToolkit. [CAMEL-AI](https://github.com/camel-ai/camel) is a multi-agent framework with a large ecosystem of toolkits. Mirage ships two of those toolkits, terminal and file, backed by a `Workspace` instead of the host. The agent's shell, file reads/writes, and search all run inside Mirage; mount RAM, S3, GDrive, Slack, etc. and the agent uses them through the same camel API. ## Install ```bash theme={null} uv add 'mirage-ai[camel]' ``` This pulls in `camel-ai>=0.2.90,<0.3` and `markitdown>=0.1.5`. Note: `mirage-ai[camel]` is mutually exclusive with `[openai]`, `[openhands]`, and `[pydantic-ai]`: CAMEL pins `pydantic<=2.12.0` while the other agent SDKs require a newer release. Pick one stack per environment. ## Usage ```python theme={null} import asyncio from camel.agents import ChatAgent from camel.messages import BaseMessage from camel.models import ModelFactory from camel.types import ModelPlatformType, ModelType from mirage import MountMode, Workspace from mirage.agents.camel import MirageFileToolkit, MirageTerminalToolkit from mirage.resource.ram import RAMResource ws = Workspace({"/": RAMResource()}, mode=MountMode.WRITE) terminal = MirageTerminalToolkit(ws) files = MirageFileToolkit(ws) model = ModelFactory.create( model_platform=ModelPlatformType.OPENAI, model_type=ModelType.GPT_5_MINI, ) agent = ChatAgent( system_message=BaseMessage.make_assistant_message( role_name="Mirage Camel Agent", content="You operate over a Mirage virtual filesystem mounted at /.", ), model=model, tools=[*terminal.get_tools(), *files.get_tools()], ) response = await asyncio.to_thread( agent.step, "Write /data/numbers.csv with 3 rows, then read it back.", ) print(response.msgs[-1].content) terminal.close() files.close() ``` ## Toolkits ### `MirageTerminalToolkit` Drop-in replacement for camel's `TerminalToolkit`. Same 6 functions, all routed through `Workspace.execute()`: | Method | Mirage translation | | ---------------------------------- | ---------------------------------------------------------------------------- | | `shell_exec(id, cmd, block=True)` | `await ws.execute(cmd)` | | `shell_exec(id, cmd, block=False)` | `await ws.execute(f"{cmd} &")`, uses Mirage's [JobTable](/python/quickstart) | | `shell_view(id)` | `jobs` (status) or `wait %N` (final stdout) | | `shell_kill_process(id)` | `kill %N` | | `shell_write_content_to_file` | `cat > path` with `stdin=` | | `shell_write_to_process` | Returns a clear error, Mirage's shell is non-interactive | No docker backend, no env-cloning, no safe-mode allowlist. Mirage Workspace already isolates execution. ### `MirageFileToolkit` Subclasses camel's `FileToolkit`, inherits all 7 public methods *and* every format writer (PDF, DOCX, JSON, CSV, HTML, ipynb, plain text). Path resolution and IO are overridden to route through Mirage; format writers operate on a tempfile and the result is pushed via `Workspace.execute(f"cat > {path}", stdin=...)`. | Method | Behavior | | ----------------------------------------- | ------------------------------------------------------------------ | | `write_to_file(title, content, filename)` | Format writer runs in tempdir → bytes pushed to Mirage path | | `read_file(file_paths)` | Bytes pulled from Mirage → tempfile → camel's MarkItDown converter | | `edit_file(file_path, old, new)` | In-place text replacement via Mirage | | `search_files(file_name, path)` | `find -name ` | | `glob_files(pattern, path)` | Same as `search_files` | | `grep_files(pattern, path)` | `grep -rn ` | ## Exports | Symbol | Purpose | | ----------------------- | ------------------------------------------------------------------------------ | | `MirageTerminalToolkit` | Camel `BaseToolkit` exposing the 6 shell tools, backed by `Workspace.execute`. | | `MirageFileToolkit` | Subclass of camel's `FileToolkit` routing path/IO through Mirage. | ## Examples * [`examples/python/agents/camel/sandbox_agent.py`](https://github.com/strukto-ai/mirage/blob/main/examples/python/agents/camel/sandbox_agent.py), `ChatAgent` with both toolkits over a RAM workspace. # Claude Agent SDK Source: https://docs.mirage.strukto.ai/python/agents/claude-agent-sdk Run Anthropic's Claude Agent SDK against a Mirage workspace via an in-process MCP server exposing execute, read, write, edit, ls, and grep tools. The [Claude Agent SDK](https://code.claude.com/docs/en/agent-sdk/) builds agents on Claude. Mirage exposes any `Workspace` to the SDK as an in-process MCP server, so every file and shell operation the agent runs is routed through Mirage instead of the host filesystem. This is distinct from [Claude Code](/python/agents/claude-code), which points the `claude` CLI at a [FUSE](/python/setup/fuse) mountpoint. Use this SDK integration when you build your own agent with `claude_agent_sdk.query()` and want Mirage tools rather than the built-in file tools. ## Install ```bash theme={null} uv add 'mirage-ai[claude-agent-sdk]' ``` ## Usage `build_options` wires a workspace into a ready-to-use `ClaudeAgentOptions`: it registers the Mirage MCP server, restricts the agent to Mirage's tools, and injects a system prompt describing the mounted paths. ```python theme={null} from claude_agent_sdk import query from mirage import Workspace from mirage.agents.claude_agent_sdk import build_options from mirage.resource.s3 import S3Config, S3Resource ws = Workspace({"/s3": S3Resource(S3Config(bucket="my-bucket"))}) async for msg in query( prompt="cat /s3/data.csv | grep error", options=build_options(ws), ): print(msg) ``` ## Composing with other MCP servers Use `MirageServer` directly to combine Mirage with other servers: ```python theme={null} from claude_agent_sdk import ClaudeAgentOptions from mirage.agents.claude_agent_sdk import MirageServer, build_system_prompt options = ClaudeAgentOptions( mcp_servers={"mirage": MirageServer(ws), "github": github_server}, allowed_tools=["mcp__mirage__*", "mcp__github__*"], tools=[], system_prompt=build_system_prompt(workspace=ws), ) ``` ## Tools | Tool | Maps to | | ----------------- | ---------------------------------------------------------------------------- | | `execute_command` | `Workspace.execute()`, the full shell pipeline (cat, grep, find, pipe, ...). | | `read` | Line-paginated file read with `offset` and `limit`. | | `write` | Create a new file (fails if it already exists). | | `edit` | Replace a string in an existing file. | | `ls` | List a directory. | | `grep` | Recursive `grep -rn` over the workspace. | ## Exports | Symbol | Purpose | | ---------------------- | ----------------------------------------------------------------------------------------------- | | `MirageServer` | In-process MCP server exposing the Mirage tools; pass to `ClaudeAgentOptions(mcp_servers=...)`. | | `build_options` | Returns a ready-to-use `ClaudeAgentOptions` backed by a workspace. | | `build_system_prompt` | Generates a system prompt that describes mounted paths to the model. | | `MIRAGE_SYSTEM_PROMPT` | The default system prompt template. | # Claude Code Source: https://docs.mirage.strukto.ai/python/agents/claude-code Run Anthropic's Claude Code CLI against any Mirage workspace by mounting it as a real filesystem via FUSE. [Claude Code](https://github.com/anthropics/claude-code) is Anthropic's CLI for Claude. It expects a real filesystem and doesn't expose a pluggable backend, so instead of an SDK integration, Mirage exposes any workspace as a real filesystem via [FUSE](/python/setup/fuse) and lets you point `claude` at the mountpoint. ## Install ```bash theme={null} uv add 'mirage-ai[fuse]' ``` Then install [Claude Code](https://docs.claude.com/en/docs/claude-code/setup) separately. ## Usage ```python theme={null} from mirage import Mount, MountBackend, MountMode, Workspace from mirage.resource.ram import RAMResource from mirage.resource.s3 import S3Config, S3Resource s3 = S3Resource(S3Config(bucket="my-bucket")) with Workspace( {"/": Mount(RAMResource(), mode=MountMode.WRITE, backend=MountBackend.FUSE), "/s3": Mount(s3, mode=MountMode.READ)}, ) as ws: print(f"cd {ws.fuse_mountpoint} && claude") input("Press Enter when done...") ``` The mountpoint behaves like a regular directory. `claude` reads, writes, runs `bash`, and patches files just like it would on a normal disk, every operation goes through Mirage's ops layer, so writes to `/s3/...` hit S3, writes to `/` stay in RAM. ## Why FUSE instead of an SDK integration? Claude Code's tool-use loop is fully internal, there's no `Backend` interface to swap. FUSE gives Mirage a single unforced way in: present a real path, let the agent be the agent. You lose: * Per-tool prompt customization * Observation hooks on tool calls * Mirage's op-record telemetry on the agent's specific calls (host syscalls aren't recorded the same way) You gain: * Zero integration effort * Compatibility with every Claude Code feature, including future ones * The same approach works for any other directory-based agent ([OpenAI Codex](/python/agents/codex), `aider`, etc.) # OpenAI Codex Source: https://docs.mirage.strukto.ai/python/agents/codex Run OpenAI's Codex CLI against any Mirage workspace by mounting it as a real filesystem via FUSE. [OpenAI Codex](https://github.com/openai/codex) can use Mirage through either an installable [TypeScript plugin](/typescript/agents/codex) or a FUSE mount. The Python integration [FUSE-mounts](/python/setup/fuse) a workspace as a normal host directory, so Codex's built-in filesystem and shell tools operate on Mirage without a separate tool server. ## Install ```bash theme={null} uv add 'mirage-ai[fuse]' ``` Then install [OpenAI Codex](https://github.com/openai/codex) separately. ## Usage ```python theme={null} from mirage import Mount, MountBackend, MountMode, Workspace from mirage.resource.ram import RAMResource with Workspace( {"/": Mount(RAMResource(), mode=MountMode.WRITE, backend=MountBackend.FUSE)}) as ws: print(f"cd {ws.fuse_mountpoint} && codex") input("Press Enter when done...") ``` The mountpoint behaves like a regular directory. Codex's built-in file and shell tools all dispatch through Mirage's ops layer. For the Codex app, open the mountpoint as the project folder instead of starting `codex` from it. ## FUSE or plugin? Use Python FUSE when you want Codex's built-in filesystem tools to see Mirage as an ordinary directory. This works in the CLI and app, but requires an OS FUSE driver and does not add Mirage's agent-level stale-write check. Use the [TypeScript plugin](/typescript/agents/codex) when you want named Mirage tools, Pi-style stale-write protection, and plugin installation through Codex. The plugin uses Mirage's standard MCP adapter internally. # Grok Build Source: https://docs.mirage.strukto.ai/python/agents/grok-build Run Grok Build against any Python Mirage workspace by mounting it as a real filesystem via FUSE. [Grok Build](https://x.ai/cli) can use Mirage through either an installable [TypeScript plugin](/typescript/agents/grok-build) or a FUSE mount. The Python integration [FUSE-mounts](/python/setup/fuse) a workspace as a normal host directory, so Grok's built-in filesystem and shell tools operate on Mirage without a separate tool server. ## Install ```bash theme={null} uv add 'mirage-ai[fuse]' ``` Install Grok Build separately. ## Usage ```python theme={null} from mirage import Mount, MountBackend, MountMode, Workspace from mirage.resource.ram import RAMResource with Workspace( {"/": Mount(RAMResource(), mode=MountMode.WRITE, backend=MountBackend.FUSE)}) as ws: print(f"cd {ws.fuse_mountpoint} && grok") input("Press Enter when done...") ``` The mountpoint behaves like a regular directory. Start `grok` there and its built-in file and shell tools dispatch through Mirage's ops layer. ## FUSE or plugin? Use Python FUSE when you want Grok's built-in tools to see Mirage as an ordinary directory. This requires an OS FUSE driver and does not add Mirage's agent-level stale-write check. Use the [TypeScript plugin](/typescript/agents/grok-build) when you want named Mirage tools, Pi-style stale-write protection, and Grok plugin installation. The plugin uses Mirage's standard MCP adapter internally. # Haystack Source: https://docs.mirage.strukto.ai/python/agents/haystack Give a Haystack Agent a bash tool over a Mirage workspace via MirageShellTool. [Haystack](https://haystack.deepset.ai) is deepset's framework for building LLM applications and agents. The integration is maintained by deepset and lives in their [haystack-core-integrations](https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/mirage) repository, so it installs as its own package rather than as a `mirage-ai` extra. ## Install ```bash theme={null} uv add mirage-haystack ``` `mirage-haystack` pins an exact `mirage-ai` version rather than tracking the latest, because Mirage is pre-1.0. Check the [integration's `pyproject.toml`](https://github.com/deepset-ai/haystack-core-integrations/blob/main/integrations/mirage/pyproject.toml) for which one, since installing it may move an existing `mirage-ai` in your environment. ## Usage Describe the mounts, wrap the workspace in a tool, hand the tool to an `Agent`. ```python theme={null} from haystack.components.agents import Agent from haystack.components.generators.chat import OpenAIChatGenerator from haystack.dataclasses import ChatMessage from haystack_integrations.tools.mirage import ( MirageMount, MirageShellTool, MirageWorkspace, ) workspace = MirageWorkspace([ MirageMount(path="/data", resource="ram"), MirageMount(path="/s3", resource="s3", config={"bucket": "my-bucket"}, read_only=True), ]) tool = MirageShellTool( workspace, allowed_commands=["ls", "cat", "grep", "head", "wc", "cp"], ) agent = Agent(chat_generator=OpenAIChatGenerator(model="gpt-4o-mini"), tools=[tool]) result = agent.run(messages=[ ChatMessage.from_user("How many lines in /s3/log.txt mention 'alert'?"), ]) print(result["messages"][-1].text) ``` The tool exposes a single `command` parameter, so the model writes ordinary bash and pipes across mounts. Its description is generated from the mount tree, so the model is told which paths exist without you writing a prompt for it. ## Guarding what the agent can do Two controls, and they are not interchangeable. `read_only=True` on a mount is the write boundary. Mirage refuses every write to that mount whatever command is used, so this is what prevents modification and deletion. `allowed_commands` restricts which command names may run. It is checked against every command Mirage would execute, including ones nested in `$(...)`, backticks and subshells, so `ls "$(rm x)"` is rejected unless `rm` is allowed too. Treat it as steering, not a sandbox: allowing a command that runs other commands (`eval`, `bash`, `sh`, `source`, `xargs`) effectively allows anything. Commands never reach the host shell either way. Mirage interprets them itself, so the blast radius is the mounts you attached. ## Exports | Symbol | Purpose | | ----------------- | -------------------------------------------------------------------------------------------------------------------------- | | `MirageWorkspace` | Declares the mount tree, and runs commands against it with `run` / `run_async`. Serializable with `to_dict` / `from_dict`. | | `MirageMount` | One mount: `path`, `resource`, `config`, `read_only`. | | `MirageShellTool` | The Haystack `Tool` that gives an `Agent` the bash surface. | | `MirageError` | Base error, with `MirageConfigError` and `MirageCommandNotAllowedError`. | ## Links * [Integration page](https://haystack.deepset.ai/integrations/mirage) on haystack.deepset.ai. * [Source and README](https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/mirage) in `haystack-core-integrations`. * [`mirage-haystack`](https://pypi.org/project/mirage-haystack) on PyPI. # Agents Source: https://docs.mirage.strukto.ai/python/agents/index Drop a Mirage workspace into the Python agent framework you already use. Most integrations ship behind an optional extra. Install only what you use. ```bash theme={null} uv add 'mirage-ai[openai]' # openai-agents uv add 'mirage-ai[deepagents]' # LangChain deepagents uv add 'mirage-ai[openhands]' # OpenHands SDK uv add 'mirage-ai[pydantic-ai]' # pydantic-ai uv add 'mirage-ai[camel]' # CAMEL-AI uv add 'mirage-ai[agno]' # Agno uv add 'mirage-ai[claude-agent-sdk]' # Claude Agent SDK ``` Haystack is the exception: deepset maintains it in their own repository, so it installs as `mirage-haystack` rather than as an extra. For the openai-agents SDK. Run Deep Agents on a Mirage workspace. For the OpenHands agent SDK. For pydantic-ai and pydantic-deepagents. For CAMEL-AI's `ChatAgent`. For Agno's `Agent` via `MirageToolkit`. For Haystack's `Agent`, via deepset's `mirage-haystack` package. Mount a workspace via FUSE and run `claude` against it. For `claude-agent-sdk` via an in-process MCP server. Same pattern as Claude Code: mount, then `codex`. Mount a workspace via FUSE and run `grok` against it. # LangChain (Deep Agents) Source: https://docs.mirage.strukto.ai/python/agents/langchain Back Deep Agents with a Mirage workspace via LangchainWorkspace. [Deep Agents](https://github.com/langchain-ai/deepagents) is LangChain's framework for long-horizon coding agents. It accepts a pluggable `backend` for filesystem and shell operations, and Mirage ships one. ## Install ```bash theme={null} uv add 'mirage-ai[deepagents]' langchain-anthropic ``` This pulls in `deepagents>=0.6.12`. Bring your own LangChain chat model (`langchain-anthropic`, `langchain-openai`, etc.). ## Usage ```python theme={null} from deepagents import create_deep_agent from langchain_anthropic import ChatAnthropic from mirage import MountMode, Workspace from mirage.agents.langchain import ( LangchainWorkspace, build_system_prompt, extract_text, ) from mirage.resource.ram import RAMResource ws = Workspace({"/": RAMResource()}, mode=MountMode.WRITE) agent = create_deep_agent( model=ChatAnthropic(model="claude-sonnet-4-20250514"), system_prompt=build_system_prompt( mount_info={"/": "In-memory filesystem (read/write)"}, ), backend=LangchainWorkspace(ws), ) result = agent.invoke({ "messages": [{"role": "user", "content": "Create /report.md and summarize."}], }) for text in extract_text(result["messages"][-1:]): print(text) ``` ## Multimodal files `read_file` can pass images, PDFs, audio, video, PPT, and PPTX files from a Mirage mount to a model as multimodal content. The selected model and provider must support the corresponding input type. Text files continue to use line-based pagination. ## Exports | Symbol | Purpose | | --------------------- | ------------------------------------------------------------------------------------------------------- | | `LangchainWorkspace` | `SandboxBackendProtocol` implementation for Deep Agents, wires reads, writes, edits, search, and shell. | | `extract_text` | Pulls the text content out of LangChain messages. | | `build_system_prompt` | Generates a system prompt that describes mounted paths to the model. | ## Examples * [`examples/python/agents/langchain/ram_pdf_deepagent.py`](https://github.com/strukto-ai/mirage/blob/main/examples/python/agents/langchain/ram_pdf_deepagent.py), RAM-backed PDF reading with no external storage credentials. * [`examples/python/agents/langchain/s3_deepagent.py`](https://github.com/strukto-ai/mirage/blob/main/examples/python/agents/langchain/s3_deepagent.py), read-only S3 exploration. * [`examples/python/agents/langchain/databricks_volume_deepagent.py`](https://github.com/strukto-ai/mirage/blob/main/examples/python/agents/langchain/databricks_volume_deepagent.py), Databricks volume exploration inside Databricks Apps or local SDK-auth setups. # OpenAI Agents SDK Source: https://docs.mirage.strukto.ai/python/agents/openai-agents Run openai-agents against a Mirage workspace using MirageShellExecutor, MirageEditor, and MirageSandboxClient. The OpenAI Agents Python SDK ([openai-agents](https://github.com/openai/openai-agents-python)) ships built-in `ShellTool` and `ApplyPatchTool` primitives, plus the newer `SandboxAgent`. Mirage provides drop-in replacements that route every shell command, patch, and sandbox call through your `Workspace` instead of the host. ## Install ```bash theme={null} uv add 'mirage-ai[openai]' ``` This pulls in `openai>=2.46` and `openai-agents>=0.18.3`. ## Tools (`ShellTool` + `ApplyPatchTool`) ```python theme={null} from agents import Agent, ApplyPatchTool, Runner, ShellTool from mirage import MountMode, Workspace from mirage.agents.openai_agents import ( MirageEditor, MirageShellExecutor, build_system_prompt, ) from mirage.resource.ram import RAMResource ws = Workspace({"/": RAMResource()}, mode=MountMode.WRITE) agent = Agent( name="Mirage RAM Agent", model="gpt-5.5-mini", instructions=build_system_prompt( mount_info={"/": "In-memory filesystem (read/write)"}, ), tools=[ ShellTool(executor=MirageShellExecutor(ws)), ApplyPatchTool(editor=MirageEditor(ws)), ], ) ``` ## Sandbox Agent For the new `SandboxAgent` API, use `MirageSandboxClient`: ```python theme={null} from agents.sandbox import SandboxAgent from mirage.agents.openai_agents import MirageSandboxClient client = MirageSandboxClient(ws) agent = SandboxAgent(name="...", model="gpt-5.5", instructions=ws.file_prompt) ``` ## Exports | Symbol | Purpose | | ---------------------- | -------------------------------------------------------------------- | | `MirageShellExecutor` | Drop-in `ShellTool` executor, runs inside `Workspace.execute()`. | | `MirageEditor` | Drop-in `ApplyPatchTool` editor, patches go through Mirage FS ops. | | `MirageSandboxClient` | Adapter for `agents.sandbox.SandboxAgent`. | | `MirageSandboxSession` | Per-conversation session bound to a workspace. | | `build_system_prompt` | Generates a system prompt that describes mounted paths to the model. | ## Examples * [`examples/python/agents/openai_agents/ram_agent.py`](https://github.com/strukto-ai/mirage/blob/main/examples/python/agents/openai_agents/ram_agent.py), RAM-only sandbox. * [`examples/python/agents/openai_agents/sandbox_agent.py`](https://github.com/strukto-ai/mirage/blob/main/examples/python/agents/openai_agents/sandbox_agent.py), `SandboxAgent` over RAM + S3 + Slack. # OpenHands Source: https://docs.mirage.strukto.ai/python/agents/openhands Run the All-Hands OpenHands SDK against a Mirage workspace using MirageWorkspace and the Mirage terminal tool. [OpenHands](https://github.com/All-Hands-AI/OpenHands) is All-Hands' agent SDK for autonomous software engineering. It models a `Workspace` and a `Terminal` tool, Mirage ships drop-in implementations of both so the agent operates entirely inside a Mirage workspace. ## Install ```bash theme={null} uv add 'mirage-ai[openhands]' ``` This pulls in `openhands-sdk>=1.36.1` and `openhands-tools>=1.36.1`. The OpenHands SDK requires Python >= 3.12; on 3.11 this extra installs nothing. ## Usage ```python theme={null} import os from openhands.sdk import LLM, Agent, Conversation, Tool from mirage import MountMode, Workspace from mirage.agents.openhands import MirageWorkspace, register_mirage_terminal from mirage.resource.ram import RAMResource ws = Workspace({"/": RAMResource()}, mode=MountMode.WRITE) llm = LLM( model=os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-6"), api_key=os.getenv("LLM_API_KEY"), ) with MirageWorkspace(workspace=ws, working_dir="/") as mirage_ws: tool_name = register_mirage_terminal(mirage_ws) agent = Agent( llm=llm, tools=[Tool(name=tool_name)], system_message=ws.file_prompt, ) conversation = Conversation(agent=agent, workspace=mirage_ws) conversation.send_message("Create /hello.txt with 'hi' and ls /.") conversation.run() ``` ## Exports | Symbol | Purpose | | -------------------------- | ------------------------------------------------------------------------ | | `MirageWorkspace` | OpenHands `Workspace` implementation backed by a Mirage workspace. | | `MirageTerminalExecutor` | Terminal executor that pipes through `Workspace.execute()`. | | `register_mirage_terminal` | Registers the Mirage terminal tool so the agent can `Tool(name=...)` it. | ## Examples * [`examples/python/agents/openhands/sandbox_agent.py`](https://github.com/strukto-ai/mirage/blob/main/examples/python/agents/openhands/sandbox_agent.py), RAM + S3 + Slack composed into one workspace. # Pydantic AI Source: https://docs.mirage.strukto.ai/python/agents/pydantic-ai Implement pydantic-ai-backend's SandboxProtocol so Pydantic AI agents can run inside a Mirage workspace. [Pydantic AI](https://github.com/pydantic/pydantic-ai) agents can use [`pydantic-ai-backend`](https://pypi.org/project/pydantic-ai-backend/)'s `SandboxProtocol` for filesystem, shell, grep, and edit operations. Mirage's `PydanticAIWorkspace` is a drop-in implementation of that protocol and also works with higher-level harnesses such as [pydantic-deepagents](https://github.com/vstorm-co/pydantic-deepagents). ## Install ```bash theme={null} uv add 'mirage-ai[pydantic-ai]' ``` This pulls in `pydantic-ai-slim[anthropic,openai]>=2.13.0` and `pydantic-ai-backend>=0.2.16`. To use it with [pydantic-deepagents](https://github.com/vstorm-co/pydantic-deepagents): ```bash theme={null} uv add pydantic-deep ``` ## Usage ```python theme={null} from dataclasses import dataclass from pydantic_ai import Agent from pydantic_ai_backends import create_console_toolset from mirage import MountMode, Workspace from mirage.agents.pydantic_ai import PydanticAIWorkspace, build_system_prompt from mirage.resource.ram import RAMResource ws = Workspace({"/": RAMResource()}, mode=MountMode.WRITE) backend = PydanticAIWorkspace(ws) @dataclass class Deps: backend: PydanticAIWorkspace agent = Agent( "openai:gpt-4.1", system_prompt=build_system_prompt( mount_info={"/": "In-memory filesystem (read/write)"}, ), deps_type=Deps, toolsets=[ create_console_toolset( image_support=True, document_support=True, ) ], ) result = agent.run_sync( "Create /hello.txt with 'hi' and cat it.", deps=Deps(backend=backend), ) print(result.output) ``` ## Exports | Symbol | Purpose | | --------------------- | -------------------------------------------------------------------- | | `PydanticAIWorkspace` | `SandboxProtocol` implementation backed by a Mirage workspace. | | `build_system_prompt` | Generates a system prompt that describes mounted paths to the model. | `PydanticAIWorkspace` routes file operations through the Ops layer directly and shell operations through `Workspace.execute()` for full pipe and flag support. Enable `image_support` and `document_support` on the console toolset to pass images and PDFs from any Mirage mount to multimodal models as native `BinaryContent`. ## Examples * [`examples/python/agents/pydantic_ai/s3_agent.py`](https://github.com/strukto-ai/mirage/blob/main/examples/python/agents/pydantic_ai/s3_agent.py), read-only S3 exploration. * [`examples/python/agents/pydantic_ai/s3_pdf_agent.py`](https://github.com/strukto-ai/mirage/blob/main/examples/python/agents/pydantic_ai/s3_pdf_agent.py), native PDF input from S3. * [`examples/python/agents/pydantic_ai/slack_pdf_agent.py`](https://github.com/strukto-ai/mirage/blob/main/examples/python/agents/pydantic_ai/slack_pdf_agent.py), native image and PDF reads from Slack. # discord Source: https://docs.mirage.strukto.ai/python/cli/discord Discord REST API client speaking the OpenClaw Discord action vocabulary. Discord REST API client speaking the OpenClaw Discord action vocabulary. Install it on the workspace and the whole tree is discoverable with `discord --help`. ## Install ```python theme={null} from mirage import Workspace from mirage.commands.cli.builtin.discord import DISCORD from mirage.core.discord.config import DiscordConfig from mirage.resource.discord import DiscordResource config = DiscordConfig(token="bot-token") ws = Workspace({"/discord": DiscordResource(config)}) ws.register_cli("discord", DISCORD, config.model_dump()) ``` Two installs under different names are two accounts. In YAML, the same install rides the `clis:` section; see the [CLI overview](/python/cli/index). ## Verbs The verbs follow the OpenClaw Discord action vocabulary (bare verbs: `send`, `read`, `edit`, `delete`, `react`, `search`, `thread-create`, `poll`); `members` and `server-info` are mirage extensions. IDs are Discord snowflakes, discoverable from the mounted tree (`__` path segments). ### Messages ```bash theme={null} discord send --channel 1256522563555819574 --text "Hello from MIRAGE" discord send --channel 1256522563555819574 --text "A reply" --reply-to 1489887688978075769 discord read --channel 1256522563555819574 --limit 20 discord edit --channel 1256522563555819574 --message 1489887688978075769 --text "Edited" discord delete --channel 1256522563555819574 --message 1489887688978075769 ``` | Verb | Flags | Writes | | -------- | ------------------------------- | ------ | | `send` | `--channel --text [--reply-to]` | yes | | `read` | `--channel [--limit]` | no | | `edit` | `--channel --message --text` | yes | | `delete` | `--channel --message` | yes | `edit` only works on messages the bot authored. ### Reactions, threads, polls ```bash theme={null} discord react --channel 1256522563555819574 --message 1489887688978075769 --emoji "👍" discord thread-create --channel 1256522563555819574 --message 1489887688978075769 --name "Budget talk" discord poll --channel 1256522563555819574 --question "Lunch?" --answer Pizza --answer Sushi --duration 24 ``` | Verb | Flags | Writes | | --------------- | --------------------------------------------------------------- | ------ | | `react` | `--channel --message --emoji` | yes | | `thread-create` | `--channel --name [--message]` | yes | | `poll` | `--channel --question --answer... [--duration] [--multiselect]` | yes | `--answer` repeats, one per poll option. ### Guild metadata and search ```bash theme={null} discord server-info --guild 1256522563555819574 discord members --guild 1256522563555819574 --query "alice" discord search --guild 1256522563555819574 --query "deploy" --channel 1256522563555819574 ``` | Verb | Flags | Writes | | ------------- | ----------------------------- | ------ | | `server-info` | `--guild` | no | | `members` | `--guild [--query]` | no | | `search` | `--guild --query [--channel]` | no | # gh Source: https://docs.mirage.strukto.ai/python/cli/gh Act on GitHub in the official CLI's vocabulary, alongside a github mount that reads the repository as files. Act on GitHub in the official [CLI](https://cli.github.com)'s vocabulary. A repository is a tree, so mirage already reads one as files: the [`github` mount](/python/setup/github) is the read half, and `ls`, `cat` and `grep` are how an agent explores it. `gh` is the write half, plus the account-level operations a filesystem has no shape for. ## Install ```python theme={null} import os from mirage import Workspace from mirage.commands.cli.builtin.gh import GH from mirage.core.github.config import GhConfig, GitHubConfig from mirage.resource.github import GitHubResource token = os.environ["GITHUB_TOKEN"] repo = GitHubResource( config=GitHubConfig(token=token), owner="acme", repo="tools", ref="main") ws = Workspace({"/repo": repo}) ws.register_cli("gh", GH, GhConfig(token=token, repo="acme/tools")) await ws.execute("gh repo view") ``` In YAML the same install rides the `clis:` section; see the [CLI overview](/python/cli/index). | Field | Meaning | | ---------- | ------------------------------------------------- | | `token` | the API token, as `GH_TOKEN` carries for real gh | | `repo` | the default repository, as `[HOST/]OWNER/REPO` | | `branch` | the current branch, for `{branch}` in an endpoint | | `base_url` | the API base, for GitHub Enterprise Server | `repo` and `branch` are what real gh reads off the current directory's git remote and checkout. A workspace has neither, so the install carries them: `repo` answers a line that names no repository, and both feed the `{owner}`/`{repo}`/`{branch}` placeholders. Two installs under different head words are two accounts. ## Reading is the mount, acting is the CLI ```bash theme={null} ls /repo/src # the tree, from the mount cat /repo/README.md # a blob, from the mount grep -r TODO /repo # the mount again gh api repos/acme/tools/contents/README.md -X PUT \ -f message='docs: fix a typo' -f content="$(base64 -w0 new.md)" -f sha= ``` A write through `gh` lands on the same repository the mount reads, but it lands **by repository name rather than by any vfs path**, so the mount has nothing to aim a per-path invalidation at. The spec declares which resource it serves, and the executor expires that mount's index after the line, so the next `cat` or `ls` refetches instead of serving the pre-write bytes. Nothing is required of the caller. ## Verbs ``` gh repo view [] gh repo fork [] --fork-name gh repo rename -R/--repo gh api -X/--method -f/--raw-field -F/--field ``` Every level answers `--help`, and `man gh`, `man gh repo` and `man gh api` render the same text from the same spec. ### repo ```bash theme={null} gh repo view # the install's repository gh repo view acme/tools gh repo view github.com/acme/tools # the host segment is accepted gh repo fork acme/tools gh repo fork acme/tools --fork-name tools-patched gh repo rename tools-v2 -R acme/tools ``` `view` prints what gh prints: a `name:` line, a `description:` line, then `--` and the README, with the separator omitted when the repository has none. For the repository object as JSON, use `gh api repos/OWNER/REPO`. `rename` takes the **new name** as the operand and the repository to rename on `-R`, which is the reverse of what the shape of the line suggests; that is upstream's grammar, not a mirage choice. `[HOST/]OWNER/REPO` is parsed from the right, so the owner and the repository are the last two segments and a leading host is dropped. The host is accepted for compatibility with lines copied from real gh but does **not** route: every call goes to the install's `base_url`. A second host means a second install. ### api `gh api` reaches every endpoint that has no typed verb, which is most of them. ```bash theme={null} gh api repos/acme/tools gh api /user gh api repos/acme/tools/issues -f title='Bug' -f body='Steps...' gh api -X GET search/code -f q='repo:acme/tools TODO' gh api repos/acme/tools/issues -F draft=false -F milestone=3 gh api repos/acme/tools/contents/NOTES.md -X DELETE -f message=rm -f sha= gh api graphql -f query='{viewer{login}}' gh api 'repos/{owner}/{repo}/releases' gh api 'repos/{owner}/{repo}/branches/{branch}' ``` `{owner}`, `{repo}` and `{branch}` expand from the install, the way real gh expands them from the current repository. Any other brace pair is left exactly as typed and reaches the wire, which is gh's behavior too. Quote the endpoint so the shell does not eat the braces. The rules are gh's own: * The method is `GET` with no fields and `POST` once a field is given, unless `-X` says otherwise. * A `GET` carries its fields in the **query string**; every other method carries them in a **JSON body**. * `-f/--raw-field` is always a string. `-F/--field` reads `true`, `false`, `null` and integers as their JSON types. * A call with no fields sends **no body at all**, so a bare `DELETE` is a bare `DELETE` rather than an empty JSON object with a content type. Some endpoints read those differently. * The leading slash is optional. * A placeholder expands in an endpoint and in a `-F` value, but not in a `-f` one, which is the split gh's own `--help` describes. * A read (`GET`, `HEAD`, `OPTIONS`) leaves the mount's cache alone; only a write expires it. Output is JSON on stdout, so the rest of the shell composes with it: ```bash theme={null} gh api repos/acme/tools | jq -r .default_branch gh api repos/acme/tools/issues | jq -r '.[].title' ``` ## Divergences from upstream gh `gh` is virtualized, not wrapped, so the table below is the whole of what differs. Everything else matches `gh` 2.85. | Divergence | Why | | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | | `gh api` pretty-prints the response; real gh prints the body verbatim | the reader is usually an agent or `jq`, and an indented body is what a human reads | | `gh repo view` has no `--json`/`-q`/`-t`; the default text view matches | upstream's field set is GraphQL's, with 75 names; `gh api repos/O/R` is the JSON route | | `--paginate`, `-q/--jq`, `-H`, `--input`, `--silent`, `-i` are not built | not built yet; pipe to `jq` for selection, and paginate with `-f page=2` | | `-F` reads no `@file` values, and `key[sub]=` / `key[]=` nesting is flat | not built yet | | `repo rename` has no `-y/--yes` | there is no prompt to skip: nothing in a workspace is interactive | | `repo fork` has no `--clone`, `--remote`, `--org` | those act on a host checkout and a git remote, which a workspace has neither of | | a `HOST/` prefix is accepted but never routes | one install is one account on one host; a second host is a second install | The interactive and host-side verbs (`auth`, `gist`, `codespace`, `browse`, `repo clone`) are out of scope for a virtualized CLI, the same way they are for the other account CLIs. # git Source: https://docs.mirage.strukto.ai/python/cli/git Read and change a git repository that lives on any mount, in git's own vocabulary. Read and change a git repository that lives on any mount, in git's own vocabulary. Unlike the account CLIs, `git` needs no credentials and takes no config: it is a program tree with nothing to authenticate to, so installing it is one line. ## Install ```python theme={null} from mirage import Workspace from mirage.commands.cli.builtin.git import GIT from mirage.resource.ram import RAMResource ws = Workspace({"/repo": RAMResource()}) ws.register_cli("git", GIT) await ws.execute("git -C /repo status --short") ``` In YAML the same install rides the `clis:` section; see the [CLI overview](/python/cli/index). ## The repository is read through the mount `-C` names a directory inside a mount, and everything under it is read with the same ops any command uses. The repository is never opened from the host filesystem, so a repository on a RAM mount, a disk mount or an object store all read the same way, and packfiles, loose objects and the index are all read through the mount. `-C` defaults to the working directory, and the repository is found by walking up from there, so a path inside the tree works: ```bash theme={null} git -C /repo/src status ``` ## Verbs ### Inspect ```bash theme={null} git -C /repo status git -C /repo status --short git -C /repo status --porcelain -b git -C /repo status -uall git -C /repo log --oneline -n 20 git -C /repo log --reverse git -C /repo log -S delta git -C /repo log HEAD~2 git -C /repo log --all --oneline git -C /repo log --format='%h %an %s' git -C /repo log --pretty=fuller -n 3 git -C /repo show 265ec3a git -C /repo show --stat HEAD git -C /repo show --name-only HEAD git -C /repo show -s --format=%H HEAD git -C /repo diff HEAD~1 HEAD git -C /repo branch ``` `log` and `show` take `--pretty`/`--format` with git's grammar: the `oneline`, `short`, `medium`, `full` and `fuller` presets, plus `format:`/`tformat:` placeholder strings (a bare `%` string is `tformat:`). Placeholders cover ids (`%H %h %T %t %P %p`), author and committer fields (`%an %ae %ad %at %cn %ce %cd %ct`), the message (`%s %b %B`), decorations (`%d %D`), and `%n %% %xHH`; an unknown placeholder stays verbatim, exactly as git prints it. `log --all` walks every ref, tags peeled. `show` takes `--stat` (git's scaled diffstat table), `--name-only`, and `-s`/`--no-patch`, which suppresses every diff section just as it does in git. `status` honors `.gitignore` at every level, including negated rules, and collapses an untracked directory to one entry the way git does (`-uall` descends, `-uno` hides untracked files entirely). ### Change ```bash theme={null} git -C /repo add -A git -C /repo add src/main.py git -C /repo add -u git -C /repo reset git -C /repo reset src/main.py git -C /repo commit -m "message" git -C /repo branch topic git -C /repo branch -d topic git -C /repo branch -D topic git -C /repo checkout topic ``` | Verb | Notes | | ---------- | ----------------------------------------------------------------- | | `add` | `-A` everything, `-u` tracked only, `-f` to stage an ignored path | | `reset` | mixed only, from HEAD; a pathspec limits it to those paths | | `commit` | `-m` required, `--author "Name "` optional | | `branch` | `-d` deletes a merged branch, `-D` any branch | | `checkout` | refuses rather than overwrite work you have not committed | `commit` records `mirage ` unless `--author` says otherwise: git reads `user.name` from config files that a mount does not serve, and inventing a name would put an unreviewed one into history. ## Deliberate limits * **No network verbs.** `clone`, `fetch`, `pull` and `push` are absent. * **No `reset --hard`.** It destroys uncommitted work with no reflog here to recover it from. * **`reset` takes no revision.** Real git resets the index to any commit named as an operand; this build resets it from HEAD only and refuses a revision by name rather than doing nothing quietly. * **`checkout` refuses a staged change** rather than merging it into the target, so nothing is silently resolved. * **Diff hunk bodies can differ from git's** on the same change. Headers, mode lines and blob abbreviations match; the line grouping inside a hunk comes from a different algorithm than git's xdiff. `show --stat` line counts come from the same algorithm, so a rewritten hunk can count slightly differently than git counts it. * **`--pretty` knows the block presets, not the wire formats.** `raw`, `email`, `mboxrd` and `reference` are refused by name (`unsupported --pretty format`), not silently misrendered. `--date=` relative formats and `--decorate` are absent; decorations render only through `%d`/`%D`, matching git's piped default of no decorations. * **`log` dates are strict.** `--since`/`--until` read ISO-8601 or an epoch second; git's relative wording (`2 weeks ago`) is refused rather than misread. # GWS Source: https://docs.mirage.strukto.ai/python/cli/gws Google Workspace API client: Discovery passthroughs plus ergonomic helpers. Google Workspace API client: Discovery passthroughs plus ergonomic helpers. Install it on the workspace and the whole tree is discoverable with `gws --help`. ## Install ```python theme={null} from mirage import Workspace from mirage.commands.cli.builtin.gws import GWS from mirage.core.google.config import GoogleConfig from mirage.resource.gmail import GmailResource config = GoogleConfig(client_id="...", client_secret="...", refresh_token="...") ws = Workspace({"/mail": GmailResource(config)}) ws.register_cli("gws", GWS, config.model_dump()) ``` Two installs under different names are two accounts. In YAML, the same install rides the `clis:` section; see the [CLI overview](/python/cli/index). ## Verbs The syntax mirrors the official [Google Workspace CLI](https://github.com/googleworkspace/cli): one passthrough leaf per Discovery method, plus hand-written helpers under each service. ```bash theme={null} gws [--params JSON] [--json JSON] # API passthrough gws [flags] # ergonomic helper ``` Services: `drive`, `sheets`, `docs`, `slides`, `gmail`. The whole surface is discoverable from the shell: ```bash theme={null} gws --help # services gws drive files --help # methods on a Discovery resource gws gmail send --help # helper flags ``` ### API passthrough Passthrough commands call the corresponding API method directly: `--params` fills URL path and query parameters, `--json` is the request body, and the output is the compact API response JSON. List methods follow `nextPageToken` to the end by default (pages print as NDJSON); `--page-limit N` stops early. ```bash theme={null} # List Drive files gws drive files list --params '{"q": "name contains \'report\'"}' # Read a Gmail message raw gws gmail users messages get --params '{"userId": "me", "id": "msg123"}' # Batch-update a spreadsheet gws sheets spreadsheets batchUpdate \ --params '{"spreadsheetId": "SHEET_ID"}' \ --json '{"requests": [...]}' # Share a Drive file gws drive permissions create \ --params '{"fileId": "FILE_ID"}' \ --json '{"role": "reader", "type": "anyone"}' ``` ### Folder scope A config carrying `folder_id` scopes what the CLI **creates**, so a file it makes lands in the same folder a `GDriveResource` sharing that config mounts, and the agent's own `ls` shows what it just made: ```python theme={null} config = GoogleConfig(client_id="...", refresh_token="...", folder_id="FOLDER_ID") ws = Workspace({"/data": GDriveResource(config)}) ws.register_cli("gws", GWS, config.model_dump()) ``` ```bash theme={null} gws sheets spreadsheets create --json '{"properties": {"title": "Q3"}}' ls /data # Q3.gsheet.json ``` Three things worth knowing: * **Two divergences from the official CLI's passthrough.** `drive files create` and `copy` get `parents` defaulted into the request body, and the Docs/Sheets/Slides `create` methods have no `parents` field at all, so mirage issues a second Drive call to move the new file. Both also send `supportsAllDrives`, which is what lets a scope name a Shared Drive folder. * **An explicit `parents` array always wins**, and then nothing is injected into the query either: you typed the call, you own it. The key being present is what counts, so `"parents": []` is honored too rather than read as absent. * **Reads are not scoped.** `gws drive files list` still sees the whole account. The scope is about where new files go, not a fence. ### Helpers | Helper | Description | | --------------------- | ------------------------------------------------ | | `gws gmail send` | Send a new email (`--to --subject --body`) | | `gws gmail reply` | Reply to the sender (`--message-id --body`) | | `gws gmail reply-all` | Reply to all recipients (To + CC) | | `gws gmail forward` | Forward a message (`--message-id --to`) | | `gws gmail read` | One message as processed JSON (`--id`) | | `gws gmail triage` | Summaries for a search query (`--query --max`) | | `gws sheets read` | Read a cell range (`--spreadsheet --range`) | | `gws sheets write` | Overwrite a range (`--values` / `--json-values`) | | `gws sheets append` | Append rows after a range | | `gws docs write` | Append text to a document (`--document --text`) | ```bash theme={null} gws gmail send --to "user@example.com" --subject "Hello" --body "Hi there" gws gmail triage --query "is:unread" --max 10 gws sheets read --spreadsheet SHEET_ID --range "Sheet1!A1:C10" gws sheets append --spreadsheet SHEET_ID --values "alice,42" gws docs write --document DOC_ID --text "New paragraph" ``` # Himalaya Source: https://docs.mirage.strukto.ai/python/cli/himalaya IMAP/SMTP mail client following the pimalaya/himalaya vocabulary. IMAP/SMTP mail client following the pimalaya/himalaya vocabulary. Install it on the workspace and the whole tree is discoverable with `himalaya --help`. ## Install ```python theme={null} from mirage import Workspace from mirage.commands.cli.builtin.himalaya import HIMALAYA from mirage.core.email.config import EmailConfig from mirage.resource.email import EmailResource config = EmailConfig( imap_host="imap.example.com", smtp_host="smtp.example.com", username="agent@example.com", password="app-password", ) ws = Workspace({"/mail": EmailResource(config)}) ws.register_cli("himalaya", HIMALAYA, config.model_dump()) ``` Two installs under different names are two accounts. In YAML, the same install rides the `clis:` section; see the [CLI overview](/python/cli/index). ## Sent copies Sending is SMTP and keeps no record of itself, so the copy in your own Sent mailbox is a second, separate IMAP `APPEND` that mail clients make on your behalf. mirage makes it too, `\Seen`, on every `--send`. Which mailbox it lands in is asked, not guessed: a server that implements RFC 6154 tags one of its mailboxes `\Sent` in its folder listing, which is `[Gmail]/Sent Mail` on Gmail and `Sent Items` on Exchange. Set `sent_folder` to pin a name and skip the probe, or `save_copy=False` to file nothing. ```python theme={null} config = EmailConfig( imap_host="imap.example.com", smtp_host="smtp.example.com", username="agent@example.com", password="app-password", save_copy=True, # the default sent_folder="Sent", # unset asks the server ) ``` `--save ` overrides both for one line, and on its own (without `--send`) it files the message without sending it, which is how a draft is written. The two failure modes differ on purpose: a copy that fails *after* a successful send is a warning on stderr and exit 0, because the mail is already gone and a non-zero exit invites a retry that would send it twice; a `--save` that sends nothing fails loudly, because nothing happened yet. ## Verbs The verbs follow the [himalaya](https://github.com/pimalaya/himalaya) CLI structure: `himalaya envelope list|search` to triage, `himalaya message read|compose|send|reply|forward` to act. Messages are addressed by positional id, the mailbox by `-m/--mailbox`, and reads return JSON rather than a rendered table. Upstream aliases resolve too: `envelope ls`, `envelope sr`, `message write`, `message new`, `message fwd`. ### `himalaya envelope list` List a mailbox, most recent first. ```bash theme={null} himalaya envelope list -m INBOX --page 2 --page-size 10 ``` | Option | Required | Description | | ----------------- | -------- | ----------------------------- | | `-m, --mailbox` | no | Mailbox name (default: INBOX) | | `-p, --page` | no | Page number, starting from 1 | | `-s, --page-size` | no | Max envelopes per page (25) | Only the newest `page * page_size` messages are fetched, so listing the first page costs one page of header fetches rather than a scan of the whole mailbox. The account's `max_messages` (default 200) bounds how far back paging can reach; an `order by` is unrelated to arrival order, so a sorted search considers that whole window. ### `himalaya envelope search` Filter and sort with himalaya's own query DSL. The query is the trailing operand, so it is words rather than flags. ```bash theme={null} himalaya envelope search -m INBOX not flag seen and from alice@example.com himalaya envelope search after 2026-01-01 order by subject asc himalaya envelope search subject '"quarterly review"' ``` Conditions: `date `, `before `, `after `, `from `, `to `, `subject `, `body `, and `flag `. Combine them with `and`, `or` and `not`, group with parentheses, and sort with `order by [asc|desc]`. Three things to know about the grammar. The date conditions read the message's own `Date:` header, not the mailbox's received-at timestamp, so imported or delayed mail lands on the day it was sent. `after` is strictly greater than the given day, unlike IMAP's inclusive `SENTSINCE`. And a pattern containing spaces needs *literal* double quotes inside the shell's quoting (`'"quarterly review"'`), because the shell's own quotes are gone by the time the query reaches the parser, exactly as upstream behaves. The same paging flags as `envelope list` apply. A query that does not parse exits 1 without contacting the server. ### `himalaya message read` ```bash theme={null} himalaya message read -m INBOX 12345 himalaya message read -m INBOX 12345 --raw ``` | Option | Required | Description | | --------------- | -------- | -------------------------------- | | `` | yes | Message id, positional | | `-m, --mailbox` | no | Mailbox name (default: INBOX) | | `--raw` | no | Write the RFC 5322 bytes instead | ### `himalaya message compose` The built-in flag composer. Without `--send` it writes the assembled RFC 5322 message to stdout, so it can be piped into `message send` or into another composer. ```bash theme={null} himalaya message compose --to you@example.org --subject Hello --body Hi --send himalaya message compose --to you@example.org --subject Hello --body Hi | himalaya message send echo "the body" | himalaya message compose --to you@example.org --subject Hello --send himalaya message compose --to you@example.org --subject Report --body 'see attached' --attach /data/report.pdf --send ``` | Option | Required | Description | | --------------- | -------- | ---------------------------------------------- | | `--from` | no | Sender address (default: the account username) | | `-t, --to` | no | Recipient(s), repeatable or comma-separated | | `--cc` | no | Carbon-copy recipient(s) | | `--bcc` | no | Blind carbon-copy recipient(s) | | `-s, --subject` | no | Subject line | | `--body` | no | Inline body (falls back to stdin) | | `--attach` | no | Attachment file path, repeatable | | `--signature` | no | Signature appended after a `-- ` line | | `--send` | no | Send through SMTP instead of writing to stdout | | `--save` | no | File a copy in this mailbox (see above) | ### `himalaya message send` Sends a raw RFC 5322 message taken from the operand or from stdin. This is the sink a composer chain feeds. ```bash theme={null} himalaya message send < message.eml himalaya message send --save Sent < message.eml himalaya message compose --to you@example.org --subject Hi --body yo | himalaya message send ``` | Option | Required | Description | | -------- | -------- | --------------------------- | | `` | no | The message itself, inline | | `--save` | no | File a copy in this mailbox | ### `himalaya message reply` Fetches the source message, prefills `Re:` on the subject plus `In-Reply-To` / `References`, derives the recipient from the source's `Reply-To` (else its `From`), and quotes the source body. Like `compose`, it writes MIME to stdout unless `--send` is passed. ```bash theme={null} himalaya message reply -m INBOX 12345 --body 'Thanks for the update' --send himalaya message reply -m INBOX 12345 --cc team@example.org --body Ack --send ``` It carries every composer flag above, plus the mailbox and: | Option | Required | Description | | ---------------------- | -------- | ------------------------------------------ | | `` | yes | Source message id, positional | | `-P, --posting-style` | no | `top` (default) or `bottom` | | `-Q, --quote-headline` | no | Literal line placed before the quoted body | There is no `--all` flag, matching upstream: reply-all is spelled by naming the other recipients with `--cc`, which `message read` reports. ### `himalaya message forward` ```bash theme={null} himalaya message forward -m INBOX 12345 --to colleague@example.com --send ``` Same flags as `reply`. The subject gains `Fwd:`, `References` carries over, and `In-Reply-To` does not. ## Divergences from upstream Deliberate gaps, all of which fail loudly rather than silently: * `message add`, `copy`, `move`, `delete`, `flag add`, `attachment download`, `mailbox` and the protocol-specific subgroups (`imap`, `jmap`, `gmail`, `msgraph`, `smtp`) are not implemented. An unknown verb exits 1 with git's wording. * `message read --seen` is absent because the mount is read-only and never flips `\Seen`. * The account-level sent copy is mirage's own, and defaults on. Upstream v2 files a copy only when `--save ` names one; a mirage agent that never learned the flag still leaves the record a human sender would. Turn it off with `save_copy` per account. * Resolving the sent mailbox from the server's RFC 6154 `\Sent` tag is ahead of upstream, whose own IMAP backend still pins `INBOX` alone while it waits on `LIST RETURN (SPECIAL-USE)` support in io-imap. * `envelope list` renders JSON, not a table, so its table flags (`--max-width`, `--recipient`, `--has-attachment`) do not exist. * Body and signature files (`--body-file`, `--signature-file`) are not wired up; `--attach` is, reading each path through the workspace, with the content type guessed from a fixed extension table rather than a full mime database. Server behavior can differ too: whether `from alice` matches `alice@example.com` as a substring is up to the IMAP server, not mirage. # CLIs Source: https://docs.mirage.strukto.ai/python/cli/index Install typed command-line programs beside your mounts and let agents act on services by name. Mounts make a service readable as files; CLIs make it actionable as a program. A CLI is a typed program tree (`CLISpec`) installed on the workspace by name and separate from the mounts: an account CLI initializes from its own config, consults no mount, and takes no operand path. `git` is the credential-free tier, so it takes no config at all and reads the repository `-C` names through the mount ops. The shell dispatches a line to a CLI when its first word matches an installed name. ```python theme={null} from mirage import Workspace from mirage.commands.cli.builtin.himalaya import HIMALAYA ws = Workspace({"/mail": EmailResource(config)}) ws.register_cli("himalaya", HIMALAYA, config.model_dump()) await ws.execute("himalaya envelope list --unseen --max 5") ``` In YAML the same install rides the `clis:` section: ```yaml theme={null} mounts: /mail: resource: email config: { ... } clis: himalaya: cli: himalaya config: { ... } ``` Every level of the tree answers `--help`, unknown verbs fail with git's wording (exit 1), and missing required flags fail with argparse's wording (exit 2). Two installs under different head words are two accounts. ## Authoring your own CLI A CLI is code you point at, the same way a mount points at a resource. In application code you build a `CLISpec`, and each leaf takes the line's one `CLIInvocation`: ```python theme={null} from mirage import CLIInvocation, CLISpec from mirage.io import IOResult async def send(inv: CLIInvocation[MyConfig]): return f"sent to {inv.flags['to']}\n".encode(), IOResult() TREE = CLISpec(name="mine", config_model=MyConfig, subcommands=(CLISpec(name="send", fn=send), )) ``` The record carries both views of the line: the process view (`argv`, `stdin`, `env`) and the parsed view (`config`, `paths`, `texts`, `flags`). In YAML, `cli:` references that spec by builtin or registered name, by `module:ATTR` import, or by `./file.py:ATTR` path. A package can also publish one through the `mirage.clis` entry-point group. ### A CLI as a script `script:` points at an ordinary program instead of a spec tree, so a CLI can be authored with no mirage import at all: ```yaml theme={null} clis: pager: script: ./cli/pager.py config: { width: 80 } ``` The file's content is embedded at load (relative paths resolve next to the config file) and the program runs on the workspace's runtime world: `.py` on the first Python runtime, `.js`/`.mjs` on the first JavaScript one. What it gets is what a native binary would get. The words after the head arrive verbatim, piped input arrives on standard input, the install's config arrives as `MIRAGE_CLI_CONFIG` in the environment as JSON, and the workspace mounts are visible as ordinary files through the runtime's bridge. Its exit code becomes the line's `$?` and its stderr reaches the shell. A Python program reads that variable either way the language spells it, `os.getenv('MIRAGE_CLI_CONFIG')` or `os.environ['MIRAGE_CLI_CONFIG']`, on every Python runtime and on both host languages. How arguments and input are spelled is the runtime's own contract, the same one the `python3` and `node` commands follow: | Runtime | Arguments | Standard input | | --------------- | ------------- | -------------- | | monty (default) | `argv` global | `stdin` global | | wasi, local | `sys.argv` | `sys.stdin` | | quickjs | `scriptArgs` | `std.in` | Slot 0 is the installed name, so `pager --width 80` reads as `['pager', '--width', '80']` and a program's own messages can say `pager:` like any other tool. Two installs of one program are told apart the same way. Arguments therefore start at index 1 on monty and quickjs. The exception is `wasi` and `local`, where a real CPython runs the code as `-c` and defines `sys.argv[0]` itself; mirage cannot fill that slot, so it stays `-c` and arguments start at index 1 there too. A script CLI is one program rather than a verb tree, so it parses its own arguments, and mirage stays out of the way: it recognizes no flags of its own, so `pager --width 80` reaches the program instead of being refused, and `pager --help` is the program's to answer. A spec that declares `options` or `positional` opts back in, and then mirage parses the line, renders `--help` and `man` from the declaration, and refuses an undeclared flag; that is only reachable in code, since a YAML entry declares no grammar. `runtime:` pins which entry runs it, and `runtime: local` escalates a Python script to the host interpreter, where third-party packages are available. The sandboxed runtimes are files plus compute: no sockets, no third-party imports on monty, no Node builtins on quickjs. Snapshots carry a script CLI by value: the embedded program travels in the snapshot and `Workspace.load` rebuilds the install from it, because there is no name for the loading process to resolve. Its config travels verbatim, since a script CLI declares no config model and so declares no secrets; keep credentials in the environment rather than the install config, or supply them through `clis=` on load. ## Discovering an installed CLI An install is discoverable from inside the shell, so an agent that was never told about it can still find it. This works for your own registered CLI exactly as for a builtin one: every page is rendered from the spec, so there is nothing extra to write. ```bash theme={null} man # lists installs under "# clis", beside the mounts man linear # the tree: description, verbs, flags man linear issue create # one leaf, same text as `linear issue create --help` type linear # linear is a mirage CLI type -t linear # cli which linear # linear ``` `type -t` prints one of `keyword`, `function`, `cli` or `builtin`. `which` prints the bare name rather than a path, since mirage has no PATH, and reports a miss through exit `1` with no output, like GNU `which`. A shell function may shadow a head word, exactly as in bash. It is reversible with `unset -f`, bypassable with `command linear ...`, and `type -a linear` lists both layers. Installing and uninstalling a CLI is a host-side API only (`ws.register_cli` / `ws.unregister_cli`): there is no shell verb for it, so an agent cannot uninstall the tools it was given. ## Builtin CLIs | Program | Acts on | Vocabulary | | -------------------------------- | ---------------- | -------------------------------------------- | | [himalaya](/python/cli/himalaya) | IMAP/SMTP mail | pimalaya/himalaya (`envelope`, `message`) | | [gws](/python/cli/gws) | Google Workspace | official Google Workspace CLI | | [slack](/python/cli/slack) | Slack | OpenClaw Slack actions (`send-message`, …) | | [discord](/python/cli/discord) | Discord | OpenClaw Discord actions (`send`, `poll`, …) | | [ntn](/python/cli/ntn) | Notion | official Notion CLI (`pages`, `datasources`) | | [linear](/python/cli/linear) | Linear | noun/verb (`issue create`, `team list`) | | [gh](/python/cli/gh) | GitHub | official GitHub CLI (`repo`, `api`) | | [git](/python/cli/git) | git repositories | git (`status`, `log`, `add`, `commit`) | Reading stays on the mount (`cat`, `grep`, `jq` over the virtual files); acting goes through the CLI. The mounted tree's `__` path segments supply the IDs the CLI flags take. # linear Source: https://docs.mirage.strukto.ai/python/cli/linear Linear GraphQL API client with the noun/verb grammar of the mount commands. Linear GraphQL API client with the noun/verb grammar of the mount commands. Install it on the workspace and the whole tree is discoverable with `linear --help`. ## Install ```python theme={null} from mirage import Workspace from mirage.commands.cli.builtin.linear import LINEAR from mirage.core.linear.config import LinearConfig from mirage.resource.linear import LinearResource config = LinearConfig(api_key="lin_api_...") ws = Workspace({"/issues": LinearResource(config)}) ws.register_cli("linear", LINEAR, config.model_dump()) ``` Two installs under different names are two accounts. In YAML, the same install rides the `clis:` section; see the [CLI overview](/python/cli/index). ## Verbs The grammar keeps the noun/verb structure the mount commands spoke (`linear issue create`, `linear team list`). Issues are addressed by a positional key or ID (`linear issue get ENG-42`); every command emits normalized JSON, so output pipes straight into `jq`. ### Reads ```bash theme={null} linear team list linear team get ENG linear team members ENG linear issue list --team ENG linear issue get ENG-42 linear project list --team ENG linear project get --team ENG linear cycle list --team ENG linear cycle current --team ENG linear label list --team ENG linear comment list ENG-42 linear user list linear user get sam@example.com linear document list --team ENG linear document get --team ENG linear search "login bug" ``` `--team` accepts a team key, name, or ID. ### Writes ```bash theme={null} linear issue create --team ENG --title "Title" --description "Body" linear issue update ENG-42 --title "New title" linear issue assign ENG-42 --assignee-email user@example.com linear issue transition ENG-42 --state-name "In Review" linear issue set-priority ENG-42 --priority 2 linear issue set-project ENG-42 --project-name "Search" linear issue add-label ENG-42 --label-name "bug" linear comment add ENG-42 --body "comment" linear comment update --comment --body "edited" ``` | Verb | Notes | | -------------------- | ------------------------------------------------------------------------ | | `issue create` | `--team` and `--title` required | | `issue update` | `--title` and/or `--description` | | `issue assign` | `--assignee-email` or `--assignee-id` | | `issue transition` | `--state-name` or `--state-id` | | `issue set-priority` | `--priority 0..4` (0=none, 1=urgent, ... 4=low) | | `issue set-project` | `--project-name` or `--project` (ID) | | `issue add-label` | `--label-name` or `--label` (ID); appends to the issue's existing labels | Descriptions and comment bodies also read from stdin: `echo "body" | linear comment add ENG-42`. # ntn Source: https://docs.mirage.strukto.ai/python/cli/ntn Notion API client following the official Notion CLI grammar. Notion API client following the official Notion CLI grammar. Install it on the workspace and the whole tree is discoverable with `ntn --help` or `man ntn`. ## Install ```python theme={null} from mirage import Workspace from mirage.commands.cli.builtin.ntn import NTN from mirage.core.notion.config import NotionConfig from mirage.resource.notion import NotionResource config = NotionConfig(api_key="secret_...") ws = Workspace({"/notion": NotionResource(config)}) ws.register_cli("ntn", NTN, config.model_dump()) ``` Two installs under different names are two accounts. In YAML, the same install rides the `clis:` section; see the [CLI overview](/python/cli/index). ## Verbs The grammar matches the official [Notion CLI](https://developers.notion.com/cli) verb for verb, and every case is gated against the real `ntn` binary in CI, so what is written here is what the program does. **Ids are positional, not flags.** There is no `--page`, `--block` or `--datasource`; each verb names its own operand. ``` ntn api ... Call the public Notion API (beta) ntn auth token Print the current authentication token ntn datasources query ntn datasources resolve ntn pages get Retrieve a page as Markdown ntn pages create Create a page from Markdown content ntn pages edit Edit a page's content from Markdown ntn pages trash Trash a page ntn whoami Show the authenticated Notion user ``` There is no `ntn blocks`, `ntn comments` or `ntn search`. Those are reached through `ntn api` with the REST API's own paths, exactly as upstream reaches them. Upstream's interactive and deploy verbs (`login`, `logout`, `update`, `workers`, `notion-as-code`, `doctor`, `files`) are out of scope for a virtualized CLI. ### Pages Page bodies are **Markdown**, not property JSON. `create` takes the body on `--content` or from stdin, and the first heading becomes the title. ```bash theme={null} ntn pages get a1b2c3d4-... ntn pages get a1b2c3d4-... --json ntn pages create --content '# Title' --parent page:a1b2c3d4-... echo '# Title' | ntn pages create --parent data-source:e5f6a7b8-... ntn pages edit a1b2c3d4-... --content '# Replaced body' ntn pages trash a1b2c3d4-... --yes ``` | Verb | Operand | Options | Writes | | -------- | ----------- | ------------------------------- | ------ | | `get` | `` | `--json` | no | | `create` | none | `--content` `--parent` `--json` | yes | | `edit` | `` | `--content` `--json` | yes | | `trash` | `` | `--yes` | yes | `--parent` takes `page:`, `database:` or `data-source:`. `edit` replaces the page body wholesale. `trash` refuses without `--yes` unless a prompt can be answered, and sets `in_trash`. To set a row's **property values** rather than its body, use `ntn api`: ```bash theme={null} ntn api v1/pages/ -X PATCH \ -d '{"properties":{"Stage":{"select":{"name":"Draft"}}}}' ``` ### Data sources Since `2025-09-03` a database is a container of *data sources*, and the rows and the column schema live on the data source. `resolve` turns a database id into its data source ids; `query` accepts either in the same slot. ```bash theme={null} ntn datasources resolve e5f6a7b8-... ntn datasources query d5000000-... --limit 10 ntn datasources query d5000000-... -s 'Priority desc' ntn datasources query d5000000-... --filter '{"property":"Stage","select":{"equals":"Done"}}' ntn datasources query d5000000-... --json ``` `query` prints one tab-separated line per row: the page id, then the property values in alphabetical order by column name. The columns are the ones the returned rows actually carry, so a result set that does not cover the whole schema prints narrower. ### Raw API `ntn api` reaches every route that has no typed verb, including the only delete verb the public API has (`DELETE /v1/blocks/{id}`, which trashes a block, a page, or a database row). ```bash theme={null} ntn api v1/users/me ntn api v1/search -d '{"query":"Roadmap"}' ntn api v1/search query=Roadmap ntn api v1/blocks//children page_size==10 ntn api v1/blocks/ -X DELETE ntn api v1/comments -d '{"parent":{"page_id":"a1b2c3d4"},"rich_text":[{"text":{"content":"hi"}}]}' printf '{"query":"Roadmap"}' | ntn api v1/search ``` The body comes from exactly one source: stdin, `--data`/`-d`, or inline `path=value` / `path:=json` inputs. Naming two is an error. `name==value` stays a query parameter whatever the method is, and `Header:Value` sets a header. Any body source makes the call a POST unless `-X`/`--method` says otherwise; `GET`, `POST`, `PATCH`, `PUT` and `DELETE` are accepted. Use the `` / `` / `` from a mounted path segment as the operand. # slack Source: https://docs.mirage.strukto.ai/python/cli/slack Slack Web API client speaking the OpenClaw Slack action vocabulary. Slack Web API client speaking the OpenClaw Slack action vocabulary. Install it on the workspace and the whole tree is discoverable with `slack --help`. ## Install ```python theme={null} from mirage import Workspace from mirage.commands.cli.builtin.slack import SLACK from mirage.core.slack.config import SlackConfig from mirage.resource.slack import SlackResource config = SlackConfig(token="xoxb-...", search_token="xoxp-...") ws = Workspace({"/slack": SlackResource(config)}) ws.register_cli("slack", SLACK, config.model_dump()) ``` Two installs under different names are two accounts. In YAML, the same install rides the `clis:` section; see the [CLI overview](/python/cli/index). ## Verbs The verbs follow the OpenClaw Slack action vocabulary (kebab verbs: `send-message`, `read-messages`, `pin-message`, `list-pins`, `member-info`, `emoji-list`); `search` and `list-members` are mirage extensions. Search needs a user token (`search_token`), the rest run on the bot token. ### Messages ```bash theme={null} slack send-message --channel C04KEPWF6V7 --text "Hello from MIRAGE" slack send-message --channel C04KEPWF6V7 --thread-ts 1712345678.123456 --text "Thread reply" slack read-messages --channel C04KEPWF6V7 --limit 20 ``` | Verb | Flags | Writes | | --------------- | -------------------------------- | ------ | | `send-message` | `--channel --text [--thread-ts]` | yes | | `read-messages` | `--channel [--limit]` | no | ### Reactions and pins ```bash theme={null} slack react --channel C04KEPWF6V7 --ts 1712345678.123456 --emoji thumbsup slack reactions --channel C04KEPWF6V7 --ts 1712345678.123456 slack pin-message --channel C04KEPWF6V7 --ts 1712345678.123456 slack list-pins --channel C04KEPWF6V7 slack unpin-message --channel C04KEPWF6V7 --ts 1712345678.123456 ``` | Verb | Flags | Writes | | --------------- | ------------------------ | ------ | | `react` | `--channel --ts --emoji` | yes | | `reactions` | `--channel --ts` | no | | `pin-message` | `--channel --ts` | yes | | `unpin-message` | `--channel --ts` | yes | | `list-pins` | `--channel` | no | ### Members, emoji, search ```bash theme={null} slack member-info --user U04K21SEVR9 slack list-members --query "alice" slack emoji-list slack search --query 'from:@priya in:#general launch' --count 20 --page 1 ``` | Verb | Flags | Writes | | -------------- | ---------------------------- | ------ | | `member-info` | `--user` | no | | `list-members` | `[--query]` | no | | `emoji-list` | | no | | `search` | `--query [--count] [--page]` | no | `search` supports Slack query operators (`from:@user`, `in:#channel`, `after:YYYY-MM-DD`). # Installation Source: https://docs.mirage.strukto.ai/python/install Install Mirage for Python with mirage-ai from PyPI or uv, then add extras for S3, Redis, FUSE, Google Workspace, and other resources. ## Quick Install Install the base package into your project: ```bash theme={null} uv add mirage-ai ``` This is enough to start with local workflows such as the RAM resource and to understand the Mirage execution model. If you are not using uv: ```bash theme={null} pip install mirage-ai ``` ## Use Mirage as a CLI If you only want the `mirage` CLI (not the library), there are two convenient paths. **One-shot run with `uvx`**, no install, just runs: ```bash theme={null} uvx mirage-ai --help uvx mirage-ai workspace create workspace.yaml --id demo ``` **Persistent install with `uv tool install`**, drops `mirage` on your PATH globally: ```bash theme={null} uv tool install mirage-ai mirage --help ``` To upgrade later: ```bash theme={null} uv tool install --upgrade mirage-ai ``` ## Resource Extras Install extras when a resource needs optional dependencies: ```bash theme={null} uv add "mirage-ai[s3]" uv add "mirage-ai[r2]" uv add "mirage-ai[redis]" uv add "mirage-ai[fuse]" ``` Use the resource docs in the Python section when you are ready to connect real systems. ## Install From Source If you are contributing to Mirage or want the full local development setup: ```bash theme={null} git clone https://github.com/strukto-ai/mirage.git cd mirage/python uv sync --all-extras --no-extra camel ``` The `camel` extra conflicts with the `openai` stack, so excluding it keeps everything else installable in one shot. ## Recommended Path 1. Install the base package. 2. Follow the [Python Quickstart](/python/quickstart) with the RAM resource. 3. Add the resource extras and follow the [FUSE setup](/python/setup/fuse) if you need to expose mounts as a real filesystem. # Python Quickstart Source: https://docs.mirage.strukto.ai/python/quickstart Create a Mirage Python workspace, mount RAM as a virtual filesystem, and run shell commands that read, write, search, and transform files. ## Installation Install Mirage: ```bash theme={null} uv add mirage-ai ``` If you are not using uv: ```bash theme={null} pip install mirage-ai ``` For resources with extra dependencies, install the matching extra: ```bash theme={null} uv add "mirage-ai[s3]" ``` ## Create a Workspace Start with the RAM resource so you can try Mirage without credentials. ```python theme={null} import asyncio from mirage import MountMode, Workspace from mirage.resource.ram import RAMResource async def main() -> None: ws = Workspace({"/data": RAMResource()}, mode=MountMode.WRITE) await ws.execute('echo "hello mirage" | tee /data/hello.txt') result = await ws.execute("cat /data/hello.txt") print(await result.stdout_str()) await ws.close() asyncio.run(main()) ``` ## Run Commands Once a resource is mounted, you can use Mirage like a shell over your virtual filesystem: ```python theme={null} import asyncio from mirage import MountMode, Workspace from mirage.resource.ram import RAMResource async def main() -> None: ws = Workspace({"/data": RAMResource()}, mode=MountMode.WRITE) await ws.execute('echo \'{"name": "alice"}\' | tee /data/user.json') result = await ws.execute("ls /data/") print(await result.stdout_str()) result = await ws.execute("cat /data/hello.txt") print(await result.stdout_str()) result = await ws.execute('jq ".name" /data/user.json') print(await result.stdout_str()) result = await ws.execute("grep hello /data/hello.txt") print(await result.stdout_str()) await ws.close() asyncio.run(main()) ``` ## Estimate Before You Run `execute(..., provision=True)` returns a `ProvisionResult` instead of running the command: network/cache bytes, read ops, and a `precision` telling you how much to trust the numbers (`exact`, `range`, `unknown` -- totals under `unknown` are floors). Pipelines, `&&`/`||`, `if`/`case`, loops, and subshells aggregate automatically. ```python theme={null} plan = await ws.execute("cat /data/user.json | wc -l", provision=True) print(plan.network_read, plan.read_ops, plan.precision) ``` Read commands are estimated out of the box on every backend. When you register your own command, pass `provision=` to the `@command` decorator (reuse a helper like `make_file_read_provision(my_stat)` or `default_provision(name, my_stat)` from `mirage.commands.builtin.generic_bind`), or omit it and the planner reports `unknown`. Full semantics live in the [CLI provision docs](/home/cli#5-dry-run-with-provision). ## Output Limits To keep huge reads from flooding an agent, `cat`, `grep`, `rg`, `head`, and `tail` cap their **final** output at 2000 lines by default. When a cap fires, the agent sees the truncated bytes plus a stderr notice (`output truncated at limit (2000 lines); ...`); exit code stays 0. Caps fire only on the **terminal** command of a pipeline, so `cat big.txt | head -n 30` still shows 30 lines. ### Configure per mount Limits are per-command and per-mount. Attach them when you mount a resource by passing a `(resource, mode, {command: Limit})` tuple. Each guard sets `max_lines` / `max_bytes` (output cap) and/or `timeout_seconds` (deadline); `on_exceed` is `TRUNCATE` (default, exit 0 plus notice) or `ERROR` (exit 1 plus notice): ```python theme={null} from mirage import MountMode, Workspace from mirage.resource.ram import RAMResource from mirage.types import Limit, OnExceed ws = Workspace( { "/data": ( RAMResource(), MountMode.WRITE, { "head": Limit(max_lines=100), # cap, keep going "grep": Limit(max_lines=50, on_exceed=OnExceed.ERROR), "rg": Limit(timeout_seconds=30), # deadline }, ), }, mode=MountMode.WRITE, ) ``` The same limits are available to the CLI as a `command_limits` block in the workspace YAML. ## Next Steps * See [Python Installation](/python/install) for resource extras and the `uv` workflow. * Browse [Python Agents](/python/agents/index) to wire Mirage into the OpenAI Agents SDK, LangChain, Pydantic AI, CAMEL, and OpenHands. * Pick a real backend from [Resource Docs](/python/resource/index), such as [S3](/python/resource/s3), [Slack](/python/resource/slack), or [GitHub](/python/resource/github). # Alibaba OSS Source: https://docs.mirage.strukto.ai/python/resource/aliyun Mount an Alibaba Cloud OSS bucket via its S3-compatible API as a virtual filesystem. The Aliyun resource is a thin wrapper over the [S3 resource](/python/resource/s3). It maps an `AliyunConfig` to an `S3Config` and reuses the exact same backend, commands, and behavior as S3, it just derives the right OSS endpoint from `region`. Uses aioboto3 against Alibaba OSS's S3-compatible API. The endpoint is computed from `region` as `s3.oss-.aliyuncs.com` (e.g. `s3.oss-cn-hangzhou.aliyuncs.com`). Note the `s3.` prefix: this is the S3-compatible host, distinct from the native `oss-.aliyuncs.com`. Pass `endpoint_url` to override. ## Config ```python theme={null} import os from mirage import MountMode, Workspace from mirage.resource.aliyun import AliyunConfig, AliyunResource config = AliyunConfig( bucket=os.environ["OSS_BUCKET"], region=os.environ.get("OSS_REGION", "cn-hangzhou"), access_key_id=os.environ["OSS_ACCESS_KEY_ID"], secret_access_key=os.environ["OSS_ACCESS_KEY_SECRET"], # Optional: # endpoint_url="https://s3.oss-cn-hangzhou.aliyuncs.com", # timeout=30, # proxy="http://proxy:8080", ) resource = AliyunResource(config) ws = Workspace({"/data": resource}, mode=MountMode.READ) ``` Both `READ` and `WRITE` modes are supported. ## Example ```python theme={null} import asyncio import os from dotenv import load_dotenv from mirage import MountMode, Workspace from mirage.resource.aliyun import AliyunConfig, AliyunResource load_dotenv(".env.development") config = AliyunConfig( bucket=os.environ["OSS_BUCKET"], region=os.environ.get("OSS_REGION", "cn-hangzhou"), access_key_id=os.environ["OSS_ACCESS_KEY_ID"], secret_access_key=os.environ["OSS_ACCESS_KEY_SECRET"], ) resource = AliyunResource(config) async def main() -> None: ws = Workspace({"/data/": resource}, mode=MountMode.READ) r = await ws.execute("ls /data/") print(await r.stdout_str()) r = await ws.execute("tree /data/") print(await r.stdout_str()) if __name__ == "__main__": asyncio.run(main()) ``` ## Notes * Aliyun reports `ResourceName.S3` and routes through the same `core/s3` implementation, so the full S3 shell-command set applies. See the [S3 resource](/python/resource/s3) for the complete command reference, range reads, streaming, and the index cache fast path. * For credential setup, see [Alibaba OSS Setup](/home/setup/aliyun). # Backblaze B2 Source: https://docs.mirage.strukto.ai/python/resource/backblaze Mount a Backblaze B2 bucket via its S3-compatible API as a virtual filesystem. The Backblaze resource is a thin wrapper over the [S3 resource](/python/resource/s3). It maps a `BackblazeConfig` to an `S3Config` and reuses the exact same backend, commands, and behavior as S3, it just derives the right B2 endpoint from `region`. Uses aioboto3 against Backblaze B2's S3-compatible API. The endpoint is computed from `region` as `s3..backblazeb2.com`. Pass `endpoint_url` to override. ## Config ```python theme={null} import os from mirage import MountMode, Workspace from mirage.resource.backblaze import BackblazeConfig, BackblazeResource config = BackblazeConfig( bucket=os.environ["B2_BUCKET"], region=os.environ["B2_REGION"], # e.g. us-west-004 access_key_id=os.environ["B2_ACCESS_KEY_ID"], # B2 application keyID secret_access_key=os.environ["B2_SECRET_ACCESS_KEY"], # Optional: # endpoint_url="https://s3.us-west-004.backblazeb2.com", # timeout=30, # proxy="http://proxy:8080", ) resource = BackblazeResource(config) ws = Workspace({"/data": resource}, mode=MountMode.READ) ``` Both `READ` and `WRITE` modes are supported. ## Example ```python theme={null} import asyncio import os from dotenv import load_dotenv from mirage import MountMode, Workspace from mirage.resource.backblaze import BackblazeConfig, BackblazeResource load_dotenv(".env.development") config = BackblazeConfig( bucket=os.environ["B2_BUCKET"], region=os.environ["B2_REGION"], access_key_id=os.environ["B2_ACCESS_KEY_ID"], secret_access_key=os.environ["B2_SECRET_ACCESS_KEY"], ) resource = BackblazeResource(config) async def main() -> None: ws = Workspace({"/data/": resource}, mode=MountMode.READ) r = await ws.execute("ls /data/") print(await r.stdout_str()) r = await ws.execute("find /data/ -name '*.csv'") print(await r.stdout_str()) if __name__ == "__main__": asyncio.run(main()) ``` ## Notes * Backblaze reports `ResourceName.S3` and routes through the same `core/s3` implementation, so the full S3 shell-command set applies. See the [S3 resource](/python/resource/s3) for the complete command reference, range reads, streaming, and the index cache fast path. * For credential setup, see [Backblaze B2 Setup](/home/setup/backblaze). # Box Source: https://docs.mirage.strukto.ai/python/resource/box Mount a Box account (or a single folder of it) as a read/write Mirage filesystem with async access and shell commands. The Box resource mounts a Box account at some prefix such as `/box/`. All operations involve network I/O to the [Box v2 HTTP API](https://developer.box.com/reference/): `/2.0/folders/{id}/items` for directories, `/2.0/files/{id}/content` for content, and multipart upload / `/2.0/folders` / `delete` / `PUT` (rename or move) / `copy` for writes. Files are served as raw bytes. Both `READ` and `WRITE` modes are supported. Box addresses everything by numeric **item id**, not by path (the account root is folder id `0`). Mirage resolves each path to its id by listing folders level by level and caches the path → id mapping, so nested directories cost one API call per level on first access. Every Box item is served as its raw bytes, keeping its real name. Box's native `.boxnote` / `.boxcanvas` come back as their stored ProseMirror-style JSON (text, so `cat foo.boxnote | jq .` works), and Box's Google-Workspace files (`.gdoc` / `.gsheet` / `.gslides`) come back as the Office Open XML zips (`docx` / `xlsx` / `pptx`) Box stores them as, opaque binary like any other Office document. Box exposes no API to edit these formats from a structured payload, so Mirage does not decode them, it hands back exactly what Box stores. For a rich, editable view of Google-format documents, mount them through [Google Drive](/python/resource/gdrive) with Google credentials instead; the `gws` commands operate on Google file ids and do not apply to Box. For credential setup, see the [Box Setup](/home/setup/box) guide. The TypeScript packages ship the same backend for Node and the browser, see [Box (TypeScript)](/typescript/box). ## Config ```python theme={null} import os from mirage import MountMode, Workspace from mirage.resource.box import BoxConfig, BoxResource config = BoxConfig( # A Box developer token from the app console (~60-minute lifetime): access_token=os.environ["BOX_ACCESS_TOKEN"], # Optional: # root_folder_id="123456789", # mount a sub-folder as the root ) resource = BoxResource(config) ws = Workspace({"/box": resource}, mode=MountMode.WRITE) ``` Box supports three authentication modes: * **Developer token** (`access_token`): quickest to try; expires after \~60 minutes and cannot be refreshed programmatically. * **OAuth2 refresh** (`client_id` + `refresh_token`, optional `client_secret`): Box rotates the refresh token on each refresh; supply `on_refresh_token_rotated` to persist the new one across restarts. * **Client credentials** (`client_id` + `client_secret` + `enterprise_id`): the app authenticates as its own service account; expired tokens are simply re-fetched. The access token is cached in memory and refreshed \~5 minutes before expiry (refresh and client-credentials modes). ### Config Reference | Field | Required | Description | | ---------------- | -------- | --------------------------------------------------------------------------------- | | `access_token` | One of\* | Box developer token (skips the refresh flow) | | `client_id` | One of\* | Box app client id (with `refresh_token`, or with `client_secret`+`enterprise_id`) | | `client_secret` | No | Box app client secret | | `refresh_token` | One of\* | Long-lived OAuth2 refresh token (with `client_id`) | | `enterprise_id` | No | Enterprise id for the client-credentials grant | | `root_folder_id` | No | Mount a sub-folder as the root (default `0`, the account root) | | `endpoint` | No | Base URL overriding the real `api.box.com` hosts (test fakes) | \* Provide one of: `access_token`; `client_id` + `refresh_token`; or `client_id` + `client_secret` + `enterprise_id`. ## Mount a subfolder Pass `root_folder_id` to expose a single Box folder as the mount root instead of the whole account. Every command and FUSE/VFS op is scoped to that folder; paths outside it are unreachable. Folder ids are stable across renames and moves, and are visible in the Box web URL (`app.box.com/folder/`), so an id-based mount survives reorganization that a path prefix would not. ```python theme={null} config = BoxConfig( access_token=os.environ["BOX_ACCESS_TOKEN"], root_folder_id="123456789", ) ``` ## Cache The Box resource caches directory listings via `IndexCacheStore` (24-hour TTL) to hold the path → id mapping and reduce repeated API calls during traversal; file reads go through the standard read cache (`caches_reads = True`). Writes invalidate the affected listings so subsequent reads see the new state. ## Example ```python theme={null} import asyncio import os from mirage import MountMode, Workspace from mirage.resource.box import BoxConfig, BoxResource config = BoxConfig(access_token=os.environ["BOX_ACCESS_TOKEN"]) resource = BoxResource(config) async def main() -> None: ws = Workspace({"/box": resource}, mode=MountMode.WRITE) r = await ws.execute("ls /box/") print(await r.stdout_str()) r = await ws.execute("grep -rl report /box/docs/") print(await r.stdout_str()) # Plain text from a Box Note: # A Box Note comes back as raw ProseMirror JSON: r = await ws.execute("cat /box/notes/standup.boxnote | jq .") print(await r.stdout_str()) await ws.close() asyncio.run(main()) ``` # Ceph (Rados Gateway) Source: https://docs.mirage.strukto.ai/python/resource/ceph Mount a self-hosted Ceph Rados Gateway bucket as a virtual filesystem. The Ceph resource is a thin wrapper over the [S3 resource](/python/resource/s3). It maps a `CephConfig` to an `S3Config` and reuses the exact same backend, commands, and behavior as S3, it just points at your Ceph Rados Gateway `endpoint_url` and uses path-style addressing by default. Ceph RGW is self-hosted, so `endpoint_url` is **required** (there is no region-derived host). Uses aioboto3 against the gateway's S3-compatible API. ## Config ```python theme={null} import os from mirage import MountMode, Workspace from mirage.resource.ceph import CephConfig, CephResource config = CephConfig( bucket=os.environ["CEPH_BUCKET"], endpoint_url=os.environ["CEPH_ENDPOINT_URL"], access_key_id=os.environ["CEPH_ACCESS_KEY_ID"], secret_access_key=os.environ["CEPH_SECRET_ACCESS_KEY"], # Optional: # region="us-east-1", # default # path_style=True, # default # timeout=30, # proxy="http://proxy:8080", ) resource = CephResource(config) ws = Workspace({"/ceph": resource}, mode=MountMode.READ) ``` Both `READ` and `WRITE` modes are supported. ## Example ```python theme={null} import asyncio import os from dotenv import load_dotenv from mirage import MountMode, Workspace from mirage.resource.ceph import CephConfig, CephResource load_dotenv(".env.development") config = CephConfig( bucket=os.environ["CEPH_BUCKET"], endpoint_url=os.environ["CEPH_ENDPOINT_URL"], access_key_id=os.environ["CEPH_ACCESS_KEY_ID"], secret_access_key=os.environ["CEPH_SECRET_ACCESS_KEY"], ) resource = CephResource(config) async def main() -> None: ws = Workspace({"/ceph/": resource}, mode=MountMode.READ) r = await ws.execute("ls /ceph/") print(await r.stdout_str()) r = await ws.execute("find /ceph/ -name '*.json'") print(await r.stdout_str()) if __name__ == "__main__": asyncio.run(main()) ``` ## Notes * Ceph reports `ResourceName.S3` and routes through the same `core/s3` implementation, so the full S3 shell-command set applies. See the [S3 resource](/python/resource/s3) for the complete command reference, range reads, streaming, and the index cache fast path. * For credential setup, see [Ceph Setup](/home/setup/ceph). # Chroma Source: https://docs.mirage.strukto.ai/python/resource/chroma Mount a ChromaDB collection as a read-only virtual filesystem. The Chroma resource exposes an existing ChromaDB collection as text files mounted at a prefix such as `/knowledge/`. It is useful when you already have a chunked knowledge base in Chroma and want agents to use normal filesystem commands like `ls`, `cat`, `grep`, `find`, and `chroma-query`. For collection setup, see [Chroma Setup](/python/setup/chroma). ## Config ```python theme={null} import os from mirage import MountMode, Workspace from mirage.resource.chroma import ChromaConfig, ChromaResource config = ChromaConfig( host=os.environ.get("CHROMA_HOST", "localhost"), port=int(os.environ.get("CHROMA_PORT", "8000")), ssl=os.environ.get("CHROMA_SSL", "false").lower() == "true", collection_name=os.environ["CHROMA_COLLECTION"], slug_field=os.environ.get("CHROMA_SLUG_FIELD", "page_slug"), chunk_index_field=os.environ.get("CHROMA_CHUNK_INDEX_FIELD", "chunk_index"), ) resource = ChromaResource(config=config) ws = Workspace({"/knowledge/": resource}, mode=MountMode.READ) ``` The resource is read-only and does not support snapshots. ## Filesystem Layout Chroma paths come from the path tree document stored in the collection with ID `__path_tree__`. The document body must be a JSON object whose keys are virtual file paths below the mount prefix. ```json theme={null} { "README.md": { "size": 1024 }, "guides/quickstart.md": { "size": 4096, "updated_at": "2026-01-03T00:00:00Z" } } ``` Mounted at `/knowledge/`, this becomes: ```text theme={null} /knowledge/ README.md guides/ quickstart.md ``` Mirage infers folders from path segments and stores the tree in the index cache. The `size`, `created_at`, and `updated_at` metadata values are used by tree and listing operations when present. ## Reading Documents Each file is assembled from Chroma chunk documents whose metadata slug matches the path. Chunks are sorted by the configured chunk index field and joined with a single newline. ```bash theme={null} ls /knowledge/ tree /knowledge/ find /knowledge/ -type f cat /knowledge/guides/quickstart.md head -n 20 /knowledge/guides/quickstart.md tail -n 20 /knowledge/guides/quickstart.md grep -in "billing" /knowledge/ ``` ## Exact Search with Grep `grep` uses Chroma document filtering as a coarse prefilter when possible, then applies Mirage's grep matching over assembled file text. Scoped paths restrict the candidate files before querying Chroma. ```bash theme={null} grep "refund" /knowledge/policies/ grep -i "quickstart" /knowledge/ grep -n "API key" /knowledge/guides/quickstart.md ``` ## Vector Search The `chroma-query` command calls Chroma's native vector query API. Use it when semantic similarity matters more than exact text matching. ```bash theme={null} chroma-query "how do I get started" /knowledge/ chroma-query --top-k 5 "billing policy" /knowledge/policies/ chroma-query --top-k 3 "API authentication" /knowledge/guides/quickstart.md ``` Results are emitted one hit per line: ```text theme={null} /knowledge/guides/quickstart.md 0.82 Use an API key to authenticate requests. ``` Columns are path, score, and chunk text. The score is derived from Chroma's returned distance as `1 - distance`. ## Cache The resource uses Mirage's index cache for the virtual tree. The first directory listing or path resolution fetches `__path_tree__`; later `ls`, `find`, `tree`, and path resolution reuse the cached tree until the index cache expires or the workspace is recreated. File content is still read from Chroma when commands materialize document text. ## Examples Runnable examples live under `examples/python/chroma/`: * [`chroma.py`](https://github.com/StruktoAI/mirage/blob/main/examples/python/chroma/chroma.py) — command workflow with `ls`, `tree`, `find`, `cat`, `head`, `tail`, `grep`, and `chroma-query`. * [`chroma_vfs.py`](https://github.com/StruktoAI/mirage/blob/main/examples/python/chroma/chroma_vfs.py) — in-process VFS workflow with `os.listdir()`, `open()`, and `os.path.*`. ## Shell Commands | Command | Notes | | --------------- | ------------------------------------------------------ | | `ls` | List folders and files from the path tree | | `tree` | Print the mounted path tree | | `find` | Search the virtual tree by name, type, depth, and size | | `cat` | Read full file text assembled from Chroma chunks | | `head` / `tail` | Read the first or last lines/bytes | | `grep` | Exact or regex matching over assembled file text | | `chroma-query` | Vector retrieval through Chroma | # Databricks Volume Source: https://docs.mirage.strukto.ai/python/resource/databricks_volume Mount a Databricks Unity Catalog volume as a filesystem. `DatabricksVolumeResource` exposes files from a Unity Catalog volume through Mirage's standard filesystem interface. Agents can list, stat, read, stream, glob, and write files under the configured volume root. For auth and environment setup, see [Databricks Volume Setup](/python/setup/databricks). ## Config ```python theme={null} from mirage import MountMode, Workspace from mirage.resource.databricks_volume import ( DatabricksVolumeConfig, DatabricksVolumeResource, ) resource = DatabricksVolumeResource(DatabricksVolumeConfig( catalog="main", schema="default", volume="agent_files", root_path="/reports", )) ws = Workspace({"/dbx": resource}, mode=MountMode.READ) ``` | Field | Default | Notes | | ----------- | -------- | --------------------------------------------- | | `catalog` | required | Unity Catalog catalog name. | | `schema` | required | Unity Catalog schema name. | | `volume` | required | Unity Catalog volume name. | | `root_path` | `/` | Subdirectory inside the volume to expose. | | `host` | `None` | Optional workspace host override. | | `token` | `None` | Optional PAT override. Redacted in snapshots. | | `profile` | `None` | Optional Databricks SDK profile name. | | `timeout` | `30` | Request timeout in seconds. | ## Mount mode `read` or `write`. ## Filesystem layout Mirage maps the configured mount prefix onto the configured volume subtree. Given: ```python theme={null} DatabricksVolumeConfig( catalog="main", schema="default", volume="agent_files", root_path="/reports/2026", ) ``` and mount prefix `/dbx/`, the volume path: ```text theme={null} /Volumes/main/default/agent_files/reports/2026/q1/summary.md ``` appears in Mirage as: ```text theme={null} /dbx/q1/summary.md ``` `root_path` is normalized before use, and Mirage rejects any virtual path that would escape above that configured subtree. ## Supported operations Reads: `readdir`, `stat`, `exists`, `read_bytes`, `read_stream`, `range_read`, and glob resolution. Writes: `write`, `create`, `mkdir`, `rmdir`, `unlink`, recursive `rm`, `cp`, and `mv`. `mv`/`cp` are non-atomic download + upload — the Files API has no server-side rename. ## Shell Commands The Databricks Volume resource supports shell commands that operate on real file content. Reads use the Files API with range requests, so commands like `head -c BYTES` avoid downloading the whole object. The supported set is scoped to commands that work over the volume API (no compression, encoding, or local-only utilities). ### Read Commands | Command | Notes | | --------------- | ------------------------------------------ | | `cat` | Read file content | | `head` / `tail` | First/last N lines | | `grep` / `rg` | Pattern search (file or directory level) | | `jq` | Query JSON fields | | `wc` | Line/word/byte counts | | `stat` | File metadata (name, size, type, modified) | | `find` | Recursive search with `-name`, `-maxdepth` | | `tree` | Directory tree view | | `nl` | Number lines | ### Text Processing | Command | Notes | | ------- | ------------------------------- | | `awk` | Pattern scanning and processing | | `sed` | Stream editor | | `tr` | Translate or delete characters | | `sort` | Sort lines | | `uniq` | Remove duplicate lines | | `cut` | Extract fields/columns | | `diff` | Compare files line by line | ### File Operations | Command | Notes | | ------- | ------------------------------------------------ | | `cp` | Copy files (non-atomic download + upload) | | `mv` | Move/rename files (non-atomic download + upload) | | `rm` | Remove files (recursive for directories) | | `mkdir` | Create directories | | `touch` | Create empty file or update timestamp | ### Path Utilities | Command | Notes | | ------- | ----------------------- | | `ls` | List directory contents | ## Snapshot behavior `token` is redacted in resource state. Loading a snapshot back requires an override config that provides fresh credentials if the runtime auth chain does not already supply them. ## Databricks Apps For Databricks Apps, prefer SDK-default auth and keep Mirage in-process: ```python theme={null} config = DatabricksVolumeConfig( catalog="main", schema="default", volume="agent_files", ) resource = DatabricksVolumeResource(config) ws = Workspace({"/dbx/": resource}, mode=MountMode.READ) ``` This does not require FUSE. The agent can access the mounted workspace through Mirage's backend adapters or `Workspace.execute(...)`. ## Example ```python theme={null} import asyncio from mirage import MountMode, Workspace from mirage.resource.databricks_volume import ( DatabricksVolumeConfig, DatabricksVolumeResource, ) resource = DatabricksVolumeResource(DatabricksVolumeConfig( catalog="main", schema="default", volume="agent_files", root_path="/reports", )) async def main() -> None: ws = Workspace({"/dbx/": resource}, mode=MountMode.READ) r = await ws.execute("ls /dbx/") print(await r.stdout_str()) r = await ws.execute("find /dbx/ -name '*.md'") print(await r.stdout_str()) r = await ws.execute('head -n 20 "/dbx/q1/summary.md"') print(await r.stdout_str()) r = await ws.execute('stat "/dbx/q1/summary.md"') print(await r.stdout_str()) if __name__ == "__main__": asyncio.run(main()) ``` See: * `examples/python/databricks_volume/databricks_volume.py` * `examples/python/agents/langchain/databricks_volume_deepagent.py` ## Use Cases * **Agents in Databricks Apps**: mount a Unity Catalog volume in-process so an agent can read and write governed files without FUSE or hardcoded credentials. * **Reading governed datasets**: expose a reports or dataset subtree through `root_path` and query it with shell commands. * **Sandboxed volume access**: scope an agent to a single volume (and optional `root_path`) so it cannot read or write outside that subtree. * **Writing agent outputs back**: persist generated files to the volume with `write`, `cp`, and `mv`. # Dify Source: https://docs.mirage.strukto.ai/python/resource/dify Mount a Dify Knowledge dataset as a read-only virtual filesystem. The Dify resource exposes completed Dify Knowledge documents as text files mounted at a prefix such as `/knowledge/`. Each file is assembled from the document's completed, enabled segments. For API key setup, see [Dify Setup](/python/setup/dify). ## Config ```python theme={null} import os from mirage import MountMode, Workspace from mirage.resource.dify import DifyConfig, DifyResource config = DifyConfig( api_key=os.environ["DIFY_API_KEY"], base_url=os.environ.get("DIFY_BASE_URL", "https://api.dify.ai/v1"), dataset_id=os.environ["DIFY_DATASET_ID"], slug_metadata_name=os.environ.get("DIFY_SLUG_METADATA_NAME", "slug"), ) resource = DifyResource(config=config) ws = Workspace({"/knowledge/": resource}, mode=MountMode.READ) ``` The resource is read-only and does not support snapshots. ## Filesystem Layout Dify documents are mapped to paths using the configured slug metadata field. The default metadata field name is `slug`. If a document has no configured slug metadata, Mirage falls back to the Dify document name. ```text theme={null} /knowledge/ README.md guides/ quickstart.md api.md policies/ support.md ``` Example mapping: | Dify document | Metadata | Mirage path | | ------------- | --------------------------- | --------------------------------- | | `Quickstart` | `slug=guides/quickstart.md` | `/knowledge/guides/quickstart.md` | | `README.md` | no slug | `/knowledge/README.md` | ### Creating Slug Metadata in Dify In the Dify Knowledge UI, open a document and add a metadata item whose name matches `slug_metadata_name`. The default name is `slug`. ```text theme={null} name: slug value: guides/quickstart.md ``` For a custom Dify metadata name: ```python theme={null} config = DifyConfig( api_key=os.environ["DIFY_API_KEY"], base_url=os.environ.get("DIFY_BASE_URL", "https://api.dify.ai/v1"), dataset_id=os.environ["DIFY_DATASET_ID"], slug_metadata_name="path", ) ``` Then use: ```text theme={null} name: path value: guides/quickstart.md ``` Mirage reads `doc_metadata` from Dify and uses this value as the document's virtual path below the mount prefix. Use these rules for slug metadata values: * Use `/` to create folders. * Do not use empty segments, `.`, or `..`. * Keep each slug metadata value unique across the dataset. * Do not use a path that is also needed as a folder. For example, `guides` and `guides/quickstart.md` cannot both be document paths. Only visible documents are included: * `enabled` is `true` * `indexing_status` is `completed` * `archived` is `false` ## Reading Documents `cat`, `head`, `tail`, and `grep` read Dify document segments. Segment content is joined with a single newline between chunks. ```bash theme={null} ls /knowledge/ find /knowledge/ -type f cat /knowledge/guides/quickstart.md head -n 20 /knowledge/guides/quickstart.md grep -i "billing" /knowledge/guides/quickstart.md wc /knowledge/guides/quickstart.md ``` ## Search The `search` command calls Dify's dataset retrieval API. Use it when meaning matters more than exact text matching. ```bash theme={null} search "how do I reset my password" /knowledge/ search --method hybrid --top-k 5 "billing policy" /knowledge/policies/ search --method semantic --threshold 0.4 "quickstart" /knowledge/guides/*.md ``` Supported methods: | Method | Dify retrieval mode | | ---------- | ------------------- | | `semantic` | semantic search | | `fulltext` | full text search | | `hybrid` | hybrid search | | `keyword` | keyword search | `top-k` is capped at `100`. `threshold` must be between `0` and `1`. Results are emitted one retrieval hit per block: ```text theme={null} /knowledge/policies/refunds:0.82 Refunds are allowed within 30 days. ``` Mirage uses the configured slug metadata field to derive the path, and falls back to the Dify document name when that metadata is missing. Multiple hits from the same document remain separate records. Scoped search uses Dify metadata filtering. Documents with the configured slug metadata field are filtered by that field; name-based documents are filtered by `document_name`, which requires Dify Built-in Fields to be enabled in dataset metadata. ### Scoped Search Requirements Mirage converts a scoped search path into Dify metadata filters: | Mirage target | Dify metadata filter | | ----------------------------------------- | ---------------------------------------------------------------------- | | Document with configured slug metadata | ` in [...]` | | Document without configured slug metadata | `document_name in [...]` | | Folder or glob | `` / `document_name` filters for all matched files | To make scoped search reliable: 1. Add the configured slug metadata field to every document. 2. Enable Dify **Built-in Fields** in the dataset metadata settings. 3. Ensure `document_name` is available if you rely on name-based paths. If Built-in Fields are disabled, scoped search against documents without the configured slug metadata field may return empty results even though `cat`, `grep`, and `find` can still see the same file. ## Cache The resource uses Mirage's index cache for the virtual tree. Directory listings and path resolution reuse the cached document tree until the index expires or the workspace is recreated. If documents are edited directly in Dify, repeated operations can temporarily see cached paths and metadata. Document content is still read from Dify when commands materialize file data. ## Example ```python theme={null} import asyncio import os from dotenv import load_dotenv from mirage import MountMode, Workspace from mirage.resource.dify import DifyConfig, DifyResource load_dotenv(".env.development") config = DifyConfig( api_key=os.environ["DIFY_API_KEY"], base_url=os.environ.get("DIFY_BASE_URL", "https://api.dify.ai/v1"), dataset_id=os.environ["DIFY_DATASET_ID"], slug_metadata_name=os.environ.get("DIFY_SLUG_METADATA_NAME", "slug"), ) resource = DifyResource(config=config) async def main() -> None: ws = Workspace({"/knowledge/": resource}, mode=MountMode.READ) r = await ws.execute("ls /knowledge/") print(await r.stdout_str()) r = await ws.execute("find /knowledge/ -type f | head -n 10") print(await r.stdout_str()) r = await ws.execute('search --method hybrid --top-k 5 "getting started" /knowledge/') print(await r.stdout_str()) if __name__ == "__main__": asyncio.run(main()) ``` A runnable version is available at `examples/python/dify/dify.py`. ## Shell Commands | Command | Notes | | --------------- | ---------------------------------------------------------- | | `ls` | List folders and documents | | `cat` | Read full document text from completed segments | | `head` / `tail` | Read the first or last lines/bytes | | `grep` | Exact or regex matching over streamed document text | | `find` | Search the virtual tree by name, type, depth, and size | | `wc` | Count lines, words, bytes, characters, and max line length | | `search` | Semantic/full-text/hybrid/keyword retrieval through Dify | # DigitalOcean Spaces Source: https://docs.mirage.strukto.ai/python/resource/digitalocean Mount a DigitalOcean Spaces bucket via its S3-compatible API as a virtual filesystem. The DigitalOcean resource is a thin wrapper over the [S3 resource](/python/resource/s3). It maps a `DigitalOceanConfig` to an `S3Config` and reuses the exact same backend, commands, and behavior as S3, it just derives the right Spaces endpoint from `region`. Uses aioboto3 against DigitalOcean Spaces' S3-compatible API. The endpoint is computed from `region` as `.digitaloceanspaces.com` (e.g. `nyc3.digitaloceanspaces.com`). Pass `endpoint_url` to override. ## Config ```python theme={null} import os from mirage import MountMode, Workspace from mirage.resource.digitalocean import DigitalOceanConfig, DigitalOceanResource config = DigitalOceanConfig( bucket=os.environ["DO_SPACE"], region=os.environ.get("DO_REGION", "nyc3"), access_key_id=os.environ["DO_ACCESS_KEY_ID"], secret_access_key=os.environ["DO_SECRET_ACCESS_KEY"], # Optional: # endpoint_url="https://nyc3.digitaloceanspaces.com", # timeout=30, # proxy="http://proxy:8080", ) resource = DigitalOceanResource(config) ws = Workspace({"/data": resource}, mode=MountMode.READ) ``` Both `READ` and `WRITE` modes are supported. ## Example ```python theme={null} import asyncio import os from dotenv import load_dotenv from mirage import MountMode, Workspace from mirage.resource.digitalocean import DigitalOceanConfig, DigitalOceanResource load_dotenv(".env.development") config = DigitalOceanConfig( bucket=os.environ["DO_SPACE"], region=os.environ.get("DO_REGION", "nyc3"), access_key_id=os.environ["DO_ACCESS_KEY_ID"], secret_access_key=os.environ["DO_SECRET_ACCESS_KEY"], ) resource = DigitalOceanResource(config) async def main() -> None: ws = Workspace({"/data/": resource}, mode=MountMode.READ) r = await ws.execute("ls /data/") print(await r.stdout_str()) r = await ws.execute("tree /data/") print(await r.stdout_str()) if __name__ == "__main__": asyncio.run(main()) ``` ## Notes * DigitalOcean reports `ResourceName.S3` and routes through the same `core/s3` implementation, so the full S3 shell-command set applies. See the [S3 resource](/python/resource/s3) for the complete command reference, range reads, streaming, and the index cache fast path. * For credential setup, see [DigitalOcean Setup](/home/setup/digitalocean). # Discord Source: https://docs.mirage.strukto.ai/python/resource/discord Mount Discord guilds, channels, messages, attachments, and members as a Mirage virtual filesystem for Python agents. The Discord resource exposes guild, channel, and member data as a virtual filesystem mounted at some prefix such as `/discord/`. For token setup, see [Discord Setup](/python/setup/discord). ## Config ```python theme={null} import os from mirage import MountMode, Workspace from mirage.resource.discord import DiscordConfig, DiscordResource config = DiscordConfig(token=os.environ["DISCORD_BOT_TOKEN"]) resource = DiscordResource(config=config) ws = Workspace({"/discord": resource}, mode=MountMode.READ) ``` ## Filesystem Layout ```text theme={null} /discord/ __/ channels/ __/ / chat.jsonl files/ __. ... ... members/ __.json ... ``` Example: ```text theme={null} /discord/ My Server__111222333444555666/ channels/ general__777888999000111222/ 2026-04-04/ chat.jsonl files/ screenshot__1488111222333444555.png 2026-04-05/ chat.jsonl random__777888999000111223/ 2026-04-11/ chat.jsonl members/ alice__444555666777888999.json bob__444555666777888900.json ``` Display names keep their original spelling from Discord (spaces, apostrophes, emoji are all preserved). Only `/` is replaced with `∕` (U+2215) so it cannot collide with a directory boundary. The Discord snowflake ID is appended after `__` (double underscore) on guild, channel, member, and attachment names so resource specific commands can extract it without an extra lookup, and so two same named entities never collide. Quote names containing spaces in shell commands. `stat` also exposes the ID in the `extra` dict (see [Finding IDs](#finding-ids)). ### Guilds The root lists one directory per guild the bot has access to. ### Channels `/discord//channels/` lists text channels (types 0, 5, 15). Each channel directory contains day-partitioned **directories** for the 30 days leading up to the channel's last message. Each day directory holds: * `chat.jsonl`, the day's messages (one JSON object per line). * `files/`, attachments posted on that day. Each blob is named `__.`, where the stem keeps the original filename's spelling (only `/` is replaced). The ID suffix keeps the filename collision-free. `cat`'ing a blob downloads it from the Discord CDN. The date range is derived from `last_message_id` on the channel object, so inactive channels show dates around their last activity, not today. Soft errors (403 missing permissions, 404 unknown channel, 429 rate limit) on a single day are swallowed so listings, `find`, and `grep` keep working across the rest of the tree. ### Members `/discord//members/` lists one `.json` file per member. Reading a member file returns the full member payload from the Discord API. ## Smart Commands ### grep / rg at different scopes When `grep` or `rg` target a channel or guild directory (not a specific file), they use the Discord search API instead of downloading every `.jsonl` file: ```bash theme={null} # FILE level - downloads the .jsonl, greps locally grep hello "/discord/My Server__111222333444555666/channels/general__777888999000111222/2026-04-04/chat.jsonl" # CHANNEL level - uses Discord search API (GET /guilds/{id}/messages/search) grep hello "/discord/My Server__111222333444555666/channels/general__777888999000111222/" # GUILD level - searches across all channels grep hello "/discord/My Server__111222333444555666/" ``` Scope detection is handled by `mirage/core/discord/scope.py`. ### head / tail `head` and `tail` on file-level paths use the Discord messages API directly (`GET /channels/{id}/messages`) instead of downloading the full day's history. ## Cache The Discord resource uses `IndexCacheStore` (same as RAM/S3/disk/GitHub). Index entries store guild IDs, channel IDs, and `last_message_id` for date range computation. There is no separate content cache - file content caching is handled by the workspace `IOResult` mechanism. ## Example ```python theme={null} import asyncio import os from dotenv import load_dotenv from mirage import MountMode, Workspace from mirage.resource.discord import DiscordConfig, DiscordResource load_dotenv(".env.development") config = DiscordConfig(token=os.environ["DISCORD_BOT_TOKEN"]) resource = DiscordResource(config=config) async def main(): ws = Workspace({"/discord": resource}, mode=MountMode.READ) # List guilds r = await ws.execute("ls /discord/") print(await r.stdout_str()) guild = r.stdout_str().strip().split("\n")[0].strip() # List channels r = await ws.execute(f'ls "/discord/{guild}/channels/"') print(await r.stdout_str()) ch = r.stdout_str().strip().splitlines()[0].strip() base = f"/discord/{guild}/channels/{ch}" # Read messages from a specific date r = await ws.execute(f'cat "{base}/2026-04-04/chat.jsonl" | head -n 3') print(await r.stdout_str()) # Extract usernames with jq r = await ws.execute( f'jq -r ".[] | .author.username" "{base}/2026-04-04/chat.jsonl"') print(await r.stdout_str()) # Count messages per user r = await ws.execute( f'cat "{base}/2026-04-04/chat.jsonl"' ' | jq -r ".[] | .author.username" | sort | uniq -c') print(await r.stdout_str()) # List attachments for a day and download one r = await ws.execute(f'ls "{base}/2026-04-04/files/"') print(await r.stdout_str()) r = await ws.execute( f'cat "{base}/2026-04-04/files/screenshot__1488111222333444555.png"') blob = await r.materialize_stdout() print(f"downloaded {len(blob)} bytes") # Search across channel (uses Discord search API) r = await ws.execute(f'grep hello "{base}/"') print(await r.stdout_str()) # Search across guild r = await ws.execute(f'grep hello "/discord/{guild}/"') print(await r.stdout_str()) # Navigate with cd/pwd await ws.execute(f'cd "{base}"') r = await ws.execute("pwd") print(await r.stdout_str()) # Relative paths after cd r = await ws.execute("ls | tail -n 5") print(await r.stdout_str()) if __name__ == "__main__": asyncio.run(main()) ``` See `examples/python/discord/discord.py` for the full working example. ## Finding IDs Resource-specific commands require Discord snowflake IDs (`channel_id`, `guild_id`, `message_id`). These can be extracted from the filesystem: ```bash theme={null} # Guild ID - use stat stat "/discord/My Server__111222333444555666" # → extra={"guild_id": "1256522563555819574"} # Channel ID - use stat stat "/discord/My Server__111222333444555666/channels/general__777888999000111222" # → extra={"channel_id": "1256522563555819574"} # Message ID - inside JSONL messages jq -r '.[] | "\(.id) [\(.author.username)] \(.content)"' \ "/discord/My Server__111222333444555666/channels/general__777888999000111222/2026-04-04/chat.jsonl" # → 1489887688978075769 [alice] hello world # Find a message then reply jq -r '.[] | select(.content | test("hello")) | .id' \ "/discord/My Server__111222333444555666/channels/general__777888999000111222/2026-04-04/chat.jsonl" # → 1489887688978075769 discord send --channel 1256522563555819574 \ --text "Reply" --reply-to 1489887688978075769 ``` ## Working with Large Channels Tips for efficient access on busy channels: ```bash theme={null} # Check message count per day wc -l "/discord/My Server__111222333444555666/channels/general__777888999000111222/2026-04-04/chat.jsonl" # Read only recent messages tail -n 10 "/discord/My Server__111222333444555666/channels/general__777888999000111222/2026-04-04/chat.jsonl" # Search uses Discord API at channel/guild level (no file download) grep "keyword" "/discord/My Server__111222333444555666/channels/general__777888999000111222/" # Extract specific fields jq -r '.[] | "\(.author.username): \(.content)"' \ "/discord/My Server__111222333444555666/channels/general__777888999000111222/2026-04-04/chat.jsonl" | head -n 20 # Count messages per user cat "/discord/My Server__111222333444555666/channels/general__777888999000111222/2026-04-04/chat.jsonl" \ | jq -r '.[] | .author.username' | sort | uniq -c ``` Note: `grep`/`rg` at channel or guild level uses the Discord search API instead of downloading every `.jsonl` file, making it efficient even for large channels. ## Shell Commands Standard commands available on the mounted Discord tree: | Command | Notes | | --------------- | -------------------------------------------------------------- | | `ls` | List guilds, channels, members, dates, attachments | | `cat` | Read `chat.jsonl`, member `.json`, or download an attachment | | `head` / `tail` | Smart: uses messages API for file scope | | `grep` / `rg` | Smart: uses search API for channel/guild scope (with fallback) | | `jq` | Query JSON; use `.[]` prefix for JSONL files | | `wc` | Line/word/byte counts | | `stat` | File metadata (name, size, type, ID via `extra`) | | `find` | Recursive search with `-name`, `-maxdepth` | | `tree` | Directory tree view | Acting on Discord (sending, editing, reactions, threads, polls, member and guild info, search) goes through the [discord CLI](/python/cli/discord) when installed; the mounted tree stays read-oriented. The `__` path segments supply the snowflake IDs the CLI flags take. # Disk Source: https://docs.mirage.strukto.ai/python/resource/disk Mount a local directory as a Mirage resource with read/write shell commands and path traversal protection. The Disk resource mounts a local directory at some prefix such as `/data/`. All operations are backed by real files on disk. Path resolution validates against the root boundary to prevent directory traversal escapes. ## Config ```python theme={null} from mirage import MountMode, Workspace from mirage.resource.disk import DiskResource resource = DiskResource(root="/path/to/dir") ws = Workspace({"/data": resource}, mode=MountMode.READ) ``` `DiskResource(root=...)` takes a single `root` path argument pointing to the directory to mount. Both `READ` and `WRITE` modes are supported. ## Filesystem Layout The Disk resource mirrors the structure of the `root` directory. For example, if `root="/srv/files"` contains: ```text theme={null} /srv/files/ notes.txt config.json reports/ q1.csv q2.csv ``` Then mounting at `/data/` exposes: ```text theme={null} /data/ notes.txt config.json reports/ q1.csv q2.csv ``` Paths like `../../etc/passwd` are rejected - resolution is always confined to the root boundary. ## Cache The Disk resource uses `IndexCacheStore` with `index_ttl = 60` (1 minute). Directory listings are cached for up to 60 seconds before being refreshed from disk. ## Example ```python theme={null} import asyncio import shutil import tempfile from pathlib import Path from mirage import MountMode, Workspace from mirage.resource.disk import DiskResource DATA_DIR = Path("/path/to/files") tmp = tempfile.mkdtemp() shutil.copytree(DATA_DIR, Path(tmp) / "files", dirs_exist_ok=True) resource = DiskResource(root=tmp + "/files") async def main() -> None: ws = Workspace({"/data/": resource}, mode=MountMode.READ) r = await ws.execute("ls /data/") print(await r.stdout_str()) r = await ws.execute("cat /data/example.json") print(await r.stdout_str()) r = await ws.execute("tree /data/") print(await r.stdout_str()) r = await ws.execute("find /data/ -name '*.json'") print(await r.stdout_str()) r = await ws.execute("grep example /data/example.json") print(await r.stdout_str()) r = await ws.execute("stat /data/example.json") print(await r.stdout_str()) if __name__ == "__main__": asyncio.run(main()) ``` ## Shell Commands The Disk resource supports the full set of shell commands since it operates on real file content (text, binary, JSON, CSV, etc.): ### Read Commands | Command | Notes | | --------------- | ------------------------------------------ | | `cat` | Read file content | | `head` / `tail` | First/last N lines | | `grep` / `rg` | Pattern search (file or directory level) | | `jq` | Query JSON fields | | `wc` | Line/word/byte counts | | `stat` | File metadata (name, size, type, modified) | | `find` | Recursive search with `-name`, `-maxdepth` | | `tree` | Directory tree view | | `nl` | Number lines | | `du` | Disk usage summary | | `file` | Detect file type | | `strings` | Extract printable strings from binary | | `xxd` | Hex dump | | `md5` | MD5 checksum | | `sha256sum` | SHA-256 checksum | ### Text Processing | Command | Notes | | ---------- | ------------------------------------------- | | `awk` | Pattern scanning and processing | | `sed` | Stream editor | | `tr` | Translate or delete characters | | `sort` | Sort lines | | `uniq` | Remove duplicate lines | | `cut` | Extract fields/columns | | `join` | Join lines on a common field | | `paste` | Merge lines side by side | | `column` | Columnate output | | `fold` | Wrap lines to a specified width | | `expand` | Convert tabs to spaces | | `unexpand` | Convert spaces to tabs | | `fmt` | Simple text formatter | | `rev` | Reverse lines | | `tac` | Concatenate and print in reverse | | `look` | Display lines beginning with a given string | | `shuf` | Shuffle lines | | `tsort` | Topological sort | | `comm` | Compare two sorted files | | `cmp` | Compare two files byte by byte | | `diff` | Compare files line by line | | `patch` | Apply a diff patch | | `iconv` | Character encoding conversion | ### File Operations | Command | Notes | | -------- | ------------------------------------- | | `cp` | Copy files | | `mv` | Move/rename files | | `rm` | Remove files | | `mkdir` | Create directories | | `touch` | Create empty file or update timestamp | | `ln` | Create symbolic links | | `tee` | Write stdin to file and stdout | | `mktemp` | Create temporary file | | `split` | Split file into pieces | | `csplit` | Split file by context | ### Path Utilities | Command | Notes | | ---------- | -------------------------- | | `basename` | Strip directory from path | | `dirname` | Strip filename from path | | `realpath` | Resolve path | | `readlink` | Print symbolic link target | | `ls` | List directory contents | ### Compression | Command | Notes | | -------- | --------------------- | | `gzip` | Compress files | | `gunzip` | Decompress gzip files | | `zip` | Create zip archives | | `unzip` | Extract zip archives | | `tar` | Archive files | | `zcat` | Cat compressed files | | `zgrep` | Grep compressed files | ### Encoding | Command | Notes | | -------- | -------------------- | | `base64` | Base64 encode/decode | ## Use Cases * **Local directory access**: Mount local directories for AI agents to read and process * **Sandboxed file access**: Restrict agent file operations to a specific directory tree * **FUSE mounting**: Expose disk files through a virtual FUSE mount for external tools * **Data pipelines**: Process local datasets with shell-like commands * **Development**: Test file operations against real data before deploying to cloud resources # Dropbox Source: https://docs.mirage.strukto.ai/python/resource/dropbox Mount a Dropbox account (or a single folder of it) as a read/write Mirage filesystem with async access and shell commands. The Dropbox resource mounts a Dropbox account at some prefix such as `/dropbox/`. All operations involve network I/O to the [Dropbox v2 HTTP API](https://www.dropbox.com/developers/documentation/http/documentation): `/2/files/list_folder` for directories, `/2/files/download` for content, and `/2/files/upload` / `create_folder_v2` / `delete_v2` / `move_v2` / `copy_v2` for writes. Files are served as raw bytes. Both `READ` and `WRITE` modes are supported; single-call uploads cap at \~150 MB (Dropbox's `upload` limit). `rmdir` fails `ENOTEMPTY` on a non-empty folder instead of using the API's recursive delete; `rm -r` is the recursive path. For credential setup (app key, secret, refresh token), see the [Dropbox Setup](/home/setup/dropbox) guide. The TypeScript packages ship the same backend for Node and the browser, see [Dropbox (TypeScript)](/typescript/dropbox). ## Config ```python theme={null} import os from mirage import MountMode, Workspace from mirage.resource.dropbox import DropboxConfig, DropboxResource config = DropboxConfig( client_id=os.environ["DROPBOX_APP_KEY"], client_secret=os.environ["DROPBOX_APP_SECRET"], refresh_token=os.environ["DROPBOX_REFRESH_TOKEN"], # Optional: # root_path="/Team/data", # mount a sub-folder as the root ) resource = DropboxResource(config) ws = Workspace({"/dropbox": resource}, mode=MountMode.WRITE) ``` The access token is fetched from the long-lived refresh token and cached in memory, refreshing \~5 minutes before expiry. ### Config Reference | Field | Required | Description | | ---------------- | -------- | -------------------------------------------------------------------------------------------- | | `client_id` | Yes | Dropbox app key | | `client_secret` | Yes | Dropbox app secret | | `refresh_token` | Yes | Long-lived OAuth2 refresh token | | `root_path` | No | Mount a sub-folder of the account as the root (default `/`, the account root) | | `content_search` | No | Let `grep`/`rg` narrow recursive scans via `/2/files/search_v2` (default `False`; see below) | | `endpoint` | No | Base URL overriding the real Dropbox hosts (test fakes) | ## Mount a subfolder Pass `root_path` to expose a single Dropbox folder as the mount root instead of the whole account. Every command and FUSE/VFS op is scoped to that folder; paths outside it are unreachable. ```python theme={null} config = DropboxConfig( client_id=os.environ["DROPBOX_APP_KEY"], client_secret=os.environ["DROPBOX_APP_SECRET"], refresh_token=os.environ["DROPBOX_REFRESH_TOKEN"], root_path="/Team/data", ) ``` `root_path` accepts `Team/data`, `/Team/data`, or `/Team/data/` (all normalized the same way); `..` segments are rejected. ## Search push-down By default a recursive `grep`/`rg` walks the tree and downloads every file. With `content_search=True`, both commands first ask [`/2/files/search_v2`](https://www.dropbox.com/developers/documentation/http/documentation#files-search) which files contain the pattern's literal, then download and scan only those candidates — the output stays exactly GNU because the local scan still decides every match. Regex patterns narrow on an extracted required literal; flags whose output must see every file in scope (`grep -v`, `grep -c`, `rg -v`, `rg --type/--glob`) always take the full walk, as do file operands and multi-pattern (`-e`/`-f`) runs. An empty or failed search also falls back to the full walk. ```python theme={null} config = DropboxConfig( client_id=os.environ["DROPBOX_APP_KEY"], client_secret=os.environ["DROPBOX_APP_SECRET"], refresh_token=os.environ["DROPBOX_REFRESH_TOKEN"], content_search=True, ) ``` The knob is off by default for two reasons: full-text content search is plan-gated (Dropbox Professional/Essentials/Business and up — on other plans `search_v2` silently matches file names only, so a narrowed scan would miss content matches), and Dropbox's search index lags recent writes by a short delay, so a push-down may miss files written moments earlier. Only enable it when the account's plan includes full-text search and slightly stale results are acceptable. ## Cache The Dropbox resource caches directory listings via `IndexCacheStore` (24-hour TTL) to reduce repeated API calls during traversal; file reads go through the standard read cache (`caches_reads = True`). ## Example ```python theme={null} import asyncio import os from mirage import MountMode, Workspace from mirage.resource.dropbox import DropboxConfig, DropboxResource config = DropboxConfig( client_id=os.environ["DROPBOX_APP_KEY"], client_secret=os.environ["DROPBOX_APP_SECRET"], refresh_token=os.environ["DROPBOX_REFRESH_TOKEN"], ) resource = DropboxResource(config) async def main() -> None: ws = Workspace({"/dropbox": resource}, mode=MountMode.WRITE) r = await ws.execute("ls /dropbox/") print(await r.stdout_str()) r = await ws.execute("grep -rl report /dropbox/docs/") print(await r.stdout_str()) await ws.close() asyncio.run(main()) ``` # Email Source: https://docs.mirage.strukto.ai/python/resource/email Mount an IMAP mailbox as a Mirage filesystem and send messages through SMTP from Python workspaces. The Email resource exposes any IMAP mailbox as a virtual filesystem mounted at some prefix such as `/email/`. Sending is handled via SMTP. ## Config ```python theme={null} import os from mirage import MountMode, Workspace from mirage.resource.email import EmailConfig, EmailResource config = EmailConfig( imap_host=os.environ["IMAP_HOST"], smtp_host=os.environ["SMTP_HOST"], username=os.environ["EMAIL_USERNAME"], password=os.environ["EMAIL_PASSWORD"], ) resource = EmailResource(config=config) ws = Workspace({"/email": resource}, mode=MountMode.READ) ``` ### Config Reference | Field | Type | Default | Description | | ----------- | ------ | ------- | ------------------------ | | `imap_host` | `str` | | IMAP server hostname | | `imap_port` | `int` | `993` | IMAP port | | `smtp_host` | `str` | | SMTP server hostname | | `smtp_port` | `int` | `587` | SMTP port | | `username` | `str` | | Email address / login | | `password` | `str` | | Password or app password | | `use_ssl` | `bool` | `True` | Use SSL for IMAP | ### Common Resource Settings | Resource | IMAP Host | SMTP Host | Notes | | ----------- | ----------------------- | --------------------- | -------------------------- | | Outlook/365 | `outlook.office365.com` | `smtp.office365.com` | App password or OAuth2 | | Yahoo | `imap.mail.yahoo.com` | `smtp.mail.yahoo.com` | App password required | | Fastmail | `imap.fastmail.com` | `smtp.fastmail.com` | App password | | iCloud | `imap.mail.me.com` | `smtp.mail.me.com` | App password | | ProtonMail | `127.0.0.1` (Bridge) | `127.0.0.1` (Bridge) | Requires ProtonMail Bridge | | Self-hosted | Your server hostname | Your server hostname | Whatever you configured | ## Filesystem Layout ```text theme={null} /email/ / / __.email.json __/ # only if message has attachments ``` Example: ```text theme={null} /email/ INBOX/ 2026-04-14/ Meeting_Notes__12345.email.json Meeting_Notes__12345/ report.pdf screenshot.png Simple_Email__12346.email.json 2026-04-13/ Hello__12347.email.json Sent/ 2026-04-14/ Reply__12348.email.json Drafts/ Archive/ ``` Folder directories appear at the root, using IMAP folder names (e.g., `INBOX`, `Sent`, `Drafts`). ### Date Directories Inside each folder, messages are grouped into date subdirectories formatted as `YYYY-MM-DD`. The date is derived from the message's `Date` header. ### Message Files Each message is stored as a `.email.json` file. The filename shape is: ```text theme={null} __.email.json ``` ### Attachments Messages that have attachments get a companion subdirectory with the same base name (without `.email.json`): ```text theme={null} Meeting_Notes__12345.email.json # message JSON Meeting_Notes__12345/ # attachment directory report.pdf screenshot.png ``` ## Cache The Email resource uses `IndexCacheStore` (same as Gmail, Slack, Discord, and other resources). Index entries store folder names, message UIDs, and message metadata. ## Example ```python theme={null} import asyncio import os from dotenv import load_dotenv from mirage import MountMode, Workspace from mirage.resource.email import EmailConfig, EmailResource load_dotenv(".env.development") config = EmailConfig( imap_host=os.environ["IMAP_HOST"], smtp_host=os.environ["SMTP_HOST"], username=os.environ["EMAIL_USERNAME"], password=os.environ["EMAIL_PASSWORD"], ) resource = EmailResource(config=config) async def main(): ws = Workspace({"/email": resource}, mode=MountMode.READ) # List folders r = await ws.execute("ls /email/") print(await r.stdout_str()) # List date directories in INBOX r = await ws.execute("ls /email/INBOX/") print(await r.stdout_str()) # List messages for a specific date r = await ws.execute("ls /email/INBOX/2026-04-14/") print(await r.stdout_str()) # Read a message r = await ws.execute( "cat /email/INBOX/2026-04-14/Meeting_Notes__12345.email.json") print(await r.stdout_str()) # Extract subject with jq r = await ws.execute( 'jq ".subject"' " /email/INBOX/2026-04-14/Meeting_Notes__12345.email.json") print(await r.stdout_str()) # List attachments r = await ws.execute("ls /email/INBOX/2026-04-14/Meeting_Notes__12345/") print(await r.stdout_str()) # Search across all messages r = await ws.execute('rg "quarterly" /email/INBOX/') print(await r.stdout_str()) # Tree view r = await ws.execute("tree -L 2 /email/INBOX/") print(await r.stdout_str()) # Triage unread messages r = await ws.execute("himalaya envelope list --folder INBOX --unseen --max 5") print(await r.stdout_str()) # Send an email r = await ws.execute( 'himalaya message send --to "user@example.com"' ' --subject "Hello from MIRAGE"' ' --body "This email was sent via the MIRAGE email resource."') print(await r.stdout_str()) if __name__ == "__main__": asyncio.run(main()) ``` ## Finding UIDs Resource-specific commands require message UIDs. These can be extracted from the filesystem: ```bash theme={null} # UID is embedded in filename after "__" ls /email/INBOX/2026-04-14/ # -> Meeting_Notes__12345.email.json <- uid = 12345 # Read a message then reply himalaya message read --uid 12345 --folder INBOX himalaya message reply --uid 12345 --folder INBOX --body "Thanks for the notes" ``` ## Shell Commands Standard commands available on the mounted email tree: | Command | Notes | | --------------- | ------------------------------------------ | | `ls` | List folders, dates, messages, attachments | | `cat` | Read message JSON or attachment content | | `head` / `tail` | First/last N lines | | `grep` / `rg` | Pattern search (file or directory level) | | `jq` | Query message JSON fields | | `wc` | Line/word/byte counts | | `stat` | File metadata (name, size, type) | | `find` | Recursive search with `-name`, `-maxdepth` | | `tree` | Directory tree view | | `basename` | Extract filename from path | | `dirname` | Extract directory from path | | `realpath` | Resolve path to absolute form | | `nl` | Number lines of output | Acting on mail (list, read, send, reply, forward) goes through the [himalaya CLI](/python/cli/himalaya) when installed. # GCS Source: https://docs.mirage.strukto.ai/python/resource/gcs Mount Google Cloud Storage buckets through Mirage's async S3-compatible resource layer for Python agents. The GCS resource mounts a Google Cloud Storage bucket at some prefix such as `/gcs/`. All operations involve network I/O to the remote object store. Uses aioboto3 against GCS's S3-compatible XML API via HMAC keys, inheriting all S3 resource capabilities. For credential setup, see [GCS Setup](/home/setup/gcs). ## Config ```python theme={null} import os from mirage import MountMode, Workspace from mirage.resource.gcs import GCSConfig, GCSResource config = GCSConfig( bucket=os.environ["GCS_BUCKET"], access_key_id=os.environ["GCS_ACCESS_KEY_ID"], secret_access_key=os.environ["GCS_SECRET_ACCESS_KEY"], # Optional: # endpoint_url="https://storage.googleapis.com", # region="auto", # timeout=30, # proxy="http://proxy:8080", ) resource = GCSResource(config) ws = Workspace({"/gcs": resource}, mode=MountMode.READ) ``` `GCSResource(config)` takes a `GCSConfig` object with the bucket name and HMAC credentials. Both `READ` and `WRITE` modes are supported. ## Filesystem Layout The GCS resource maps object keys to virtual paths under the mount prefix, identical to the S3 resource. GCS "directories" are prefix-based — there are no real directory objects. For example, if bucket `mirage-ai` contains: ```text theme={null} data/example.json data/example.parquet data/example.jsonl ``` Then mounting at `/gcs/` exposes: ```text theme={null} /gcs/ data/ example.json example.parquet example.jsonl ``` Path mapping: virtual `/gcs/data/example.json` maps to GCS key `data/example.json`. ## Cache The GCS resource uses `IndexCacheStore` with `index_ttl = 600` (10 minutes), same as S3. Directory listings are cached for up to 600 seconds before being refreshed from GCS. This reduces API calls for repeated directory traversals. ## Example ```python theme={null} import asyncio import os from dotenv import load_dotenv from mirage import MountMode, Workspace from mirage.resource.gcs import GCSConfig, GCSResource load_dotenv(".env.development") config = GCSConfig( bucket=os.environ["GCS_BUCKET"], access_key_id=os.environ["GCS_ACCESS_KEY_ID"], secret_access_key=os.environ["GCS_SECRET_ACCESS_KEY"], ) resource = GCSResource(config) async def main() -> None: ws = Workspace({"/gcs/": resource}, mode=MountMode.READ) r = await ws.execute("ls /gcs/") print(await r.stdout_str()) r = await ws.execute("cat /gcs/data/example.json | head -n 10") print(await r.stdout_str()) r = await ws.execute("tree /gcs/") print(await r.stdout_str()) r = await ws.execute("find /gcs/ -name '*.parquet'") print(await r.stdout_str()) r = await ws.execute("stat /gcs/data/example.json") print(await r.stdout_str()) if __name__ == "__main__": asyncio.run(main()) ``` ## Shell Commands The GCS resource supports the full set of shell commands since it operates on real file content (text, binary, JSON, CSV, etc.). Large files benefit from range reads to avoid downloading entire objects. ### Read Commands | Command | Notes | | --------------- | ------------------------------------------ | | `cat` | Read file content | | `head` / `tail` | First/last N lines | | `grep` / `rg` | Pattern search (file or directory level) | | `jq` | Query JSON fields | | `wc` | Line/word/byte counts | | `stat` | File metadata (name, size, type, modified) | | `find` | Recursive search with `-name`, `-maxdepth` | | `tree` | Directory tree view | | `nl` | Number lines | | `du` | Disk usage summary | | `file` | Detect file type | | `strings` | Extract printable strings from binary | | `xxd` | Hex dump | | `md5` | MD5 checksum | | `sha256sum` | SHA-256 checksum | ### Text Processing | Command | Notes | | ---------- | ------------------------------------------- | | `awk` | Pattern scanning and processing | | `sed` | Stream editor | | `tr` | Translate or delete characters | | `sort` | Sort lines | | `uniq` | Remove duplicate lines | | `cut` | Extract fields/columns | | `join` | Join lines on a common field | | `paste` | Merge lines side by side | | `column` | Columnate output | | `fold` | Wrap lines to a specified width | | `expand` | Convert tabs to spaces | | `unexpand` | Convert spaces to tabs | | `fmt` | Simple text formatter | | `rev` | Reverse lines | | `tac` | Concatenate and print in reverse | | `look` | Display lines beginning with a given string | | `shuf` | Shuffle lines | | `tsort` | Topological sort | | `comm` | Compare two sorted files | | `cmp` | Compare two files byte by byte | | `diff` | Compare files line by line | | `patch` | Apply a diff patch | | `iconv` | Character encoding conversion | ### File Operations | Command | Notes | | -------- | ------------------------------------- | | `cp` | Copy files | | `mv` | Move/rename files | | `rm` | Remove files | | `mkdir` | Create directories | | `touch` | Create empty file or update timestamp | | `ln` | Create symbolic links | | `tee` | Write stdin to file and stdout | | `mktemp` | Create temporary file | | `split` | Split file into pieces | | `csplit` | Split file by context | ### Path Utilities | Command | Notes | | ---------- | -------------------------- | | `basename` | Strip directory from path | | `dirname` | Strip filename from path | | `realpath` | Resolve path | | `readlink` | Print symbolic link target | | `ls` | List directory contents | ### Compression | Command | Notes | | -------- | --------------------- | | `gzip` | Compress files | | `gunzip` | Decompress gzip files | | `zip` | Create zip archives | | `unzip` | Extract zip archives | | `tar` | Archive files | | `zcat` | Cat compressed files | | `zgrep` | Grep compressed files | ### Encoding | Command | Notes | | -------- | -------------------- | | `base64` | Base64 encode/decode | ## Use Cases * **AI agents accessing GCS data**: Mount GCS buckets for agents to read and process datasets * **Data pipelines**: Read and write GCS objects with shell-like commands * **FUSE mounting**: Expose GCS buckets through a virtual FUSE mount for external tools # Google Docs Source: https://docs.mirage.strukto.ai/python/resource/gdocs Mount Google Docs as JSON-backed files so Python agents can read document structure and content through Mirage. The Google Docs resource exposes Docs documents as a virtual filesystem mounted at some prefix such as `/gdocs/`. For Google OAuth setup, see [Google Workspace Setup](/python/setup/google). ## Config ```python theme={null} import os from mirage import MountMode, Workspace from mirage.resource.gdocs import GDocsConfig, GDocsResource config = GDocsConfig( client_id=os.environ["GOOGLE_CLIENT_ID"], client_secret=os.environ["GOOGLE_CLIENT_SECRET"], refresh_token=os.environ["GOOGLE_REFRESH_TOKEN"], ) resource = GDocsResource(config=config) ws = Workspace({"/gdocs": resource}, mode=MountMode.READ) ``` ## Filesystem Layout ```text theme={null} /gdocs/ owned/ ___.gdoc.json ... shared/ ___.gdoc.json ... ``` Example: ```text theme={null} /gdocs/ owned/ 2026-04-04_Project_Plan__1AbCdEf.gdoc.json 2026-04-10_Meeting_Notes__2BcDeFg.gdoc.json shared/ 2026-04-03_Design_Notes__9XyZ.gdoc.json ``` Documents are split into `owned` (documents you created) and `shared` (documents shared with you). The filename shape is: ```text theme={null} ___.gdoc.json ``` If the modified date is unavailable, the date prefix is omitted. Reading a document file returns the full Google Docs API JSON for that document. ### Shared Drives The listing covers every corpus the account can reach, so a document that lives in a Shared Drive appears here too. A Shared Drive document has no owner (the drive owns it), so it lands under `shared`, which is what that directory means. This matches the `gdrive` mount, where Shared Drives are top-level directories: without it the same account would see a document under one Google mount and not the other, with no error explaining the difference. Drive answers an all-corpora search best-effort. When it reports that it skipped a corpus, the short listing is still returned but is not cached as the directory, so the next `ls` re-lists instead of serving the gap until the cache expires. ## Cache The Google Docs resource uses `IndexCacheStore`. Index entries store document IDs and metadata. There is no separate content cache -- file content caching is handled by the workspace `IOResult` mechanism. ## Example ```python theme={null} import asyncio import os from dotenv import load_dotenv from mirage import MountMode, Workspace from mirage.commands.cli.builtin.gws import GWS from mirage.resource.gdocs import GDocsConfig, GDocsResource load_dotenv(".env.development") config = GDocsConfig( client_id=os.environ["GOOGLE_CLIENT_ID"], client_secret=os.environ["GOOGLE_CLIENT_SECRET"], refresh_token=os.environ["GOOGLE_REFRESH_TOKEN"], ) resource = GDocsResource(config=config) async def main(): ws = Workspace({"/gdocs": resource}, mode=MountMode.READ) ws.register_cli("gws", GWS, config.model_dump()) # List structure r = await ws.execute("ls /gdocs/") print(await r.stdout_str()) # List owned documents r = await ws.execute("ls /gdocs/owned/") print(await r.stdout_str()) # Read a document r = await ws.execute( "cat /gdocs/owned/2026-04-04_Project_Plan__1AbCdEf.gdoc.json") print(await r.stdout_str()) # Extract title with jq r = await ws.execute( 'jq ".title"' " /gdocs/owned/2026-04-04_Project_Plan__1AbCdEf.gdoc.json") print(await r.stdout_str()) # Search across all documents r = await ws.execute('rg "quarterly" /gdocs/owned/') print(await r.stdout_str()) # Tree view r = await ws.execute("tree -L 1 /gdocs/") print(await r.stdout_str()) # Create a new document r = await ws.execute( 'gws docs documents create --json \'{"title":"MIRAGE Example Doc"}\'') print(await r.stdout_str()) # Append text to a document r = await ws.execute( 'gws docs write --document 1AbCdEf --text "Appended via MIRAGE."') print(await r.stdout_str()) if __name__ == "__main__": asyncio.run(main()) ``` See `examples/gdocs/gdocs.py` for the full working example. ## Shell Commands Standard commands available on the mounted Google Docs tree: | Command | Notes | | ----------------------------------- | --------------------------- | | `ls` | List owned/shared documents | | `cat` | Read document JSON | | `head` / `tail` | First/last N lines | | `grep` / `rg` | Pattern search | | `jq` | Query JSON fields | | `wc` | Line/word/byte counts | | `stat` | File metadata | | `find` | Recursive search | | `tree` | Directory tree view | | `basename` / `dirname` / `realpath` | Path utilities | | `nl` | Number lines | Acting on documents (appending text, raw API calls) goes through the [gws CLI](/python/cli/gws) when installed. # Google Drive Source: https://docs.mirage.strukto.ai/python/resource/gdrive Mount Google Drive folders and files, including Docs, Sheets, and Slides, as a Mirage virtual filesystem. The Google Drive resource exposes a Google Drive account as a virtual filesystem mounted at some prefix such as `/gdrive/`. For Google OAuth setup, see [Google Workspace Setup](/python/setup/google). ## Config ```python theme={null} import os from mirage import MountMode, Workspace from mirage.resource.gdrive import GoogleDriveConfig, GoogleDriveResource config = GoogleDriveConfig( client_id=os.environ["GOOGLE_CLIENT_ID"], client_secret=os.environ["GOOGLE_CLIENT_SECRET"], refresh_token=os.environ["GOOGLE_REFRESH_TOKEN"], ) resource = GoogleDriveResource(config=config) ws = Workspace({"/gdrive": resource}, mode=MountMode.WRITE) ``` To mount a subfolder instead of the whole Drive, set `folder_id`. The mount root becomes that folder. The folder may live in My Drive, be shared with you, sit inside a Shared Drive, or be a Shared Drive id itself; the drive scope is resolved automatically. Scoped mounts do not surface other shared drives: ```python theme={null} config = GoogleDriveConfig( client_id=os.environ["GOOGLE_CLIENT_ID"], client_secret=os.environ["GOOGLE_CLIENT_SECRET"], refresh_token=os.environ["GOOGLE_REFRESH_TOKEN"], folder_id="1AbCdEfFolderId", ) ``` ## Filesystem Layout ```text theme={null} /gdrive/ / / ... .gdoc.json .gsheet.json .gslide.json / / ``` Example: ```text theme={null} /gdrive/ Projects/ spec.pdf roadmap.gsheet.json Budget/ Q1.gsheet.json Q2.gsheet.json Notes/ meeting.gdoc.json Presentations/ quarterly_review.gslide.json Team Drive/ shared-spec.pdf data.csv report.pdf ``` The mount mirrors the actual Google Drive folder hierarchy. The root contains the Drive API `root` folder plus each Shared Drive visible to the user. Shared Drives appear as top-level directories. Duplicate names receive a `[Shared Drive]` suffix and, when needed, a numeric suffix. Subfolders appear as directories, and regular files keep their original names. ### Synthetic Extensions Google Workspace files cannot be downloaded as raw bytes, so they are exposed with synthetic extensions and read via their respective APIs: | Extension | Type | Read via | | -------------- | ------------- | ---------- | | `.gdoc.json` | Google Docs | Docs API | | `.gsheet.json` | Google Sheets | Sheets API | | `.gslide.json` | Google Slides | Slides API | Regular files (PDFs, images, CSVs, etc.) are downloaded directly from Drive. Large regular files use streaming. ## Write Support Under `MountMode.WRITE` the standard write commands work on regular files and folders: `tee`, `cp`, `mv`, `rm`, `mkdir`, `rmdir`, `touch`, `truncate`, and in-place editors such as `sed -i`. Semantics follow the other object-store mounts (GNU check-then-act: `EEXIST` on mkdir over an existing name, `mv` onto a non-empty directory fails with `ENOTEMPTY`, `cp -r` merges). Google-native files (`.gdoc.json`, `.gsheet.json`, `.gslide.json`) are read-only as bytes; writing to them fails with `EACCES`. Mutate them through the `gws` commands below instead. Drive access is per-item (shared-drive roles, folder-level grants), so a write mount can still hold items you may not edit. A mutation the API denies fails with `EACCES` (Permission denied) on that operand, like a real filesystem; the rest of the mount keeps working. ### Writing inside a Shared Drive A Shared Drive is not a read-only corner of the mount. Every Drive call mirage makes carries `supportsAllDrives`, so create, write, rename, copy and delete work the same inside a Shared Drive as in My Drive, and the commands above behave identically there. What you may actually do is decided by Drive, not by mirage: a Shared Drive has its own role model (`viewer`, `commenter`, `contributor`, `content manager`, `manager`), and some organizations restrict deletion or moving content out of the drive. mirage does not attempt to predict those rules. It issues the operation and reports the answer, so a role that forbids the change fails with `EACCES` on that operand, exactly like an unwritable item in My Drive. The practical consequence is that a write mount spanning Shared Drives is partly writable, and the boundary follows your roles rather than the mount. If you want a mount that cannot write at all, use `MountMode.READ`; if you want to scope one to a single drive, set `folder_id` to the Shared Drive id. ## Snapshots The resource supports workspace snapshots. Recorded reads capture the file's Drive revision (`headRevisionId`), and a loaded snapshot pins reads to that revision via the Drive Revisions API, so replay serves the exact bytes the agent saw. ## Cache The Google Drive resource uses `IndexCacheStore` (same as Slack, Gmail, and other resources). Index entries store folder IDs, file IDs, and file metadata. There is no separate content cache -- file content caching is handled by the workspace `IOResult` mechanism. ## Example ```python theme={null} import asyncio import os from dotenv import load_dotenv from mirage import MountMode, Workspace from mirage.commands.cli.builtin.gws import GWS from mirage.resource.gdrive import GoogleDriveConfig, GoogleDriveResource load_dotenv(".env.development") config = GoogleDriveConfig( client_id=os.environ["GOOGLE_CLIENT_ID"], client_secret=os.environ["GOOGLE_CLIENT_SECRET"], refresh_token=os.environ["GOOGLE_REFRESH_TOKEN"], ) resource = GoogleDriveResource(config=config) async def main(): ws = Workspace({"/gdrive": resource}, mode=MountMode.WRITE) ws.register_cli("gws", GWS, config.model_dump()) # List root r = await ws.execute("ls /gdrive/ | head -n 10") print(await r.stdout_str()) # Browse a subfolder r = await ws.execute("ls /gdrive/Projects/") print(await r.stdout_str()) # Read a regular file r = await ws.execute("cat /gdrive/Projects/spec.pdf | head -c 200") print(await r.stdout_str()) # Read a Google Doc title r = await ws.execute('jq ".title" /gdrive/Notes/meeting.gdoc.json') print(await r.stdout_str()) # Read a Google Sheet title r = await ws.execute( 'jq ".properties.title" /gdrive/Projects/roadmap.gsheet.json') print(await r.stdout_str()) # Read a Google Slides deck length r = await ws.execute( 'jq ".slides | length"' " /gdrive/Presentations/quarterly_review.gslide.json") print(await r.stdout_str()) # Search across all files r = await ws.execute('rg "quarterly" /gdrive/Projects/') print(await r.stdout_str()) # Tree view r = await ws.execute("tree -L 2 /gdrive/") print(await r.stdout_str()) # Create a new Google Doc r = await ws.execute( "gws docs documents create" ' --json \'{"title": "New Doc from MIRAGE"}\'') print(await r.stdout_str()) if __name__ == "__main__": asyncio.run(main()) ``` See `examples/google/gdrive.py` for the full working example. ## Shell Commands Standard commands available on the mounted Google Drive tree: | Command | Notes | | --------------- | ------------------------------------------ | | `ls` | List folders and files | | `cat` | Read file content (regular or Workspace) | | `head` / `tail` | First/last N lines or bytes | | `grep` / `rg` | Pattern search (file or directory level) | | `jq` | Query JSON fields on Workspace files | | `wc` | Line/word/byte counts | | `stat` | File metadata (name, size, type) | | `find` | Recursive search with `-name`, `-maxdepth` | | `tree` | Directory tree view | | `basename` | Extract filename from path | | `dirname` | Extract directory from path | | `realpath` | Resolve path to absolute form | | `nl` | Number lines of output | | `sort` | Sort lines | | `uniq` | Deduplicate adjacent lines | | `cut` | Extract fields/columns | | `awk` | Pattern-directed scanning | | `sed` | Stream editor | | `tr` | Translate/delete characters | | `diff` | Compare two files | | `rev` | Reverse lines | | `tac` | Reverse file line order | | `paste` | Merge lines of files | | `join` | Join lines on a common field | | `column` | Columnate output | | `comm` | Compare sorted files line by line | | `fold` | Wrap lines to a given width | | `fmt` | Reformat paragraph text | | `expand` | Convert tabs to spaces | | `unexpand` | Convert spaces to tabs | | `du` | Estimate file space usage | | `shuf` | Randomly permute lines | | `look` | Display lines beginning with a prefix | | `strings` | Extract printable strings from binary | | `base64` | Base64 encode/decode | | `md5` | MD5 checksum | | `sha256sum` | SHA-256 checksum | | `xxd` | Hex dump | | `zcat` | Read compressed files | | `zgrep` | Search compressed files | | `readlink` | Print resolved symbolic links | | `cmp` | Compare two files byte by byte | | `tsort` | Topological sort | | `file` | Detect file type | ## Acting on Drive Acting on Drive files by id (create, update, copy, delete, share, export, plus the Docs/Sheets/Slides helpers) goes through the [gws CLI](/python/cli/gws) when installed. # GitHub Source: https://docs.mirage.strukto.ai/python/resource/github Mount a GitHub repository as a read-only Mirage filesystem for agents to list, read, search, and inspect code. The GitHub resource mounts a GitHub repository as a read-only virtual filesystem. For token setup, see [GitHub Setup](/python/setup/github). ## Config ```python theme={null} import os from mirage import MountMode, Workspace from mirage.resource.github import GitHubConfig, GitHubResource config = GitHubConfig(token=os.environ["GITHUB_TOKEN"]) resource = GitHubResource( config=config, owner="my-org", repo="my-repo", ref="main") ws = Workspace({"/github": resource}, mode=MountMode.READ) ``` ## Filesystem Layout ```text theme={null} /github/ README.md pyproject.toml src/ __init__.py main.py utils.py models/ user.py item.py tests/ test_main.py ``` The filesystem mirrors the repository tree. No owner/repo/branch in the path - those are specified at mount time. ## Tree Fetching The full recursive tree is fetched on the first read, not at mount time, so constructing the resource never blocks on the network. For repos with > 100K entries, it falls back to per-directory fetching. ## Cache The GitHub resource uses `IndexCacheStore` with SHA-based fingerprinting. Content is content-addressed - if the SHA matches, the content is identical. ## Example ```python theme={null} import asyncio import os from dotenv import load_dotenv from mirage import MountMode, Workspace from mirage.resource.github import GitHubConfig, GitHubResource load_dotenv(".env.development") async def main(): config = GitHubConfig(token=os.environ["GITHUB_TOKEN"]) resource = GitHubResource( config=config, owner="my-org", repo="my-repo", ref="main") ws = Workspace({"/github": resource}, mode=MountMode.READ) # List repository root r = await ws.execute("ls /github/") print(await r.stdout_str()) # Read a file r = await ws.execute("cat /github/README.md") print(await r.stdout_str()) # Search for a pattern r = await ws.execute('rg "def main" /github/') print(await r.stdout_str()) # Tree view r = await ws.execute("tree -L 2 /github/") print(await r.stdout_str()) # File metadata with SHA r = await ws.execute("stat /github/README.md") print(await r.stdout_str()) if __name__ == "__main__": asyncio.run(main()) ``` See `examples/code/github.py` for the full working example. ## Finding SHAs Git blob SHAs are available via the `stat` command: ```bash theme={null} stat /github/README.md # -> extra={"sha": "a1b2c3d4e5f6..."} stat /github/src/main.py # -> extra={"sha": "f6e5d4c3b2a1..."} ``` ## Working with Large Repos Tips for efficient access on large repositories: ```bash theme={null} # Find files by name find /github/ -name "*.py" # Search with rg (uses GitHub code search API when applicable) rg "TODO" /github/ # Read only the first lines of a file head -n 20 /github/src/main.py # Check file sizes du /github/src/ # List deeply nested directories tree -L 3 /github/src/ ``` ## Shell Commands Standard commands available on the mounted GitHub tree: | Command | Notes | | --------------- | ------------------------------------------ | | `ls` | List files and directories | | `cat` | Read file contents | | `head` / `tail` | First/last N lines | | `grep` / `rg` | Pattern search; rg uses code search API | | `jq` | Query JSON files | | `wc` | Line/word/byte counts | | `stat` | File metadata including git SHA | | `find` | Recursive search with `-name`, `-maxdepth` | | `tree` | Directory tree view | | `diff` | Compare files | | `du` | Disk usage / file sizes | | `awk` | Text processing | | `sed` | Stream editing | | `sort` | Sort lines | | `uniq` | Deduplicate lines | | `cut` | Extract columns | | `tr` | Translate characters | | `nl` | Number lines | | `md5` | MD5 checksum | | `sha256sum` | SHA-256 checksum | | `file` | Detect file type | | `basename` | Strip directory from path | | `dirname` | Strip filename from path | | `realpath` | Resolve path | ## Search Optimization `rg` uses the GitHub code search API when the search scope exceeds 100 files and the mounted ref is the repository's default branch. This avoids downloading file contents and returns results significantly faster for large repositories. # Gmail Source: https://docs.mirage.strukto.ai/python/resource/gmail Mount Gmail mailboxes as a Mirage filesystem so Python agents can browse labels, messages, threads, and attachments. The Gmail resource exposes a Gmail account as a virtual filesystem mounted at some prefix such as `/gmail/`. For Google OAuth setup, see [Google Workspace Setup](/python/setup/google). ## Config ```python theme={null} import os from mirage import MountMode, Workspace from mirage.resource.gmail import GmailConfig, GmailResource config = GmailConfig( client_id=os.environ["GOOGLE_CLIENT_ID"], client_secret=os.environ["GOOGLE_CLIENT_SECRET"], refresh_token=os.environ["GOOGLE_REFRESH_TOKEN"], ) resource = GmailResource(config=config) ws = Workspace({"/gmail": resource}, mode=MountMode.READ) ``` ## Filesystem Layout ```text theme={null} /gmail/