Replace dsh's ctx.fs and ctx.shell capabilities with the Mirage workspace, enabling the DSH Agent to directly read and write to mounted data sources such as S3, Slack, and Redis.
- Language
- TypeScript
- License
- Apache-2.0
- Branch
- main
Install
$ dsh plugin --profile web add github:strukto-ai/mirage/typescript/packages/dshRun the command above in your terminal to install this plugin via the dsh CLI. You can switch Profile in the top-right corner. New to dsh? Read the beginner tutorial
One-Line Description
Replace DeepSeek Harness's file and shell backends with a Mirage workspace, enabling dsh's file tools and bash tools to directly read/write data sources like S3, Slack, Redis, and Gmail that are connected via mounts, instead of the host machine's local disk.
Core Features
- Take over dsh's filesystem interface (ctx.fs): read, write, edit, stat, listDir, etc. all go through the Mirage workspace, with all operations first passing through the mount point mode and policy layer
- Take over dsh's shell interface (ctx.shell): bash tools execute Mirage's shell instead, with POSIX tools (grep, ls, cat, cp, etc.) uniformly available across multiple mount sources
- Provide declarative mounts: list resources (resource registration name, mode, config) in profile's cordis.patch.yml, with secrets like tokens resolved via
!!js process.env.Xat mount time - Support sandboxed Python runtimes (monty, etc.) and ~50 built-in backends (RAM, Disk, S3, Redis, Slack, Gmail, GDrive, Notion, Postgres, SSH, etc.), scripts can run under any mounted path
- Default mount a
/tmp(RAM memory disk, exec mode), usable for temporary files and Python script output - Support persistent shell: passing
sessionIdto the shell plugin preserves cd, export, and function definitions across multiple invocations
Technical Implementation
- Language: TypeScript (DSH integration package)
- Key Dependencies:
@struktoai/mirage-node(provides Workspace, resource registry, shell executor),@deepseek-ai/cordis(plugin orchestration),@deepseek-ai/dsh-fsand@deepseek-ai/dsh-shell(DSH's file and shell capability seams) - Architecture Pattern: Loaded via dsh's bundle mechanism (package.json's
dsh.bundle.patchpoints to cordis.patch.yml); the patch disables dsh's built-in five plugin lines (fs-sandbox,bash-sandbox,pwsh-sandbox,tool-pwsh,tool-fs-search) and inserts Mirage's three-piece set (MirageServiceholds the workspace,MirageFileSystemandMirageShellExecutoreach declareinject = ['mirage']to get it) - Entry Files:
typescript/packages/dsh/cordis.patch.yml(DSH bundle entry),typescript/packages/dsh/src/plugin/{service,fs,shell}.ts(each Cordis plugin's default export)
Use Cases
You want dsh's Agent to operate not on the host machine's native filesystem but on existing objects in external data sources like S3, Slack, Notion, Redis; or you want to give an Agent session a clean, host-isolated "virtual workspace" where the Agent can read/write, run Python scripts, and sync output files. Also suitable for directly embedding an existing Mirage workspace into the DSH system for use.
Prerequisites and Compatibility
| Dependency | Minimum Version | Description |
|---|---|---|
| Node.js | >=20.10.0 | Declared by package's engines field |
| DSH | 0.1.0-rc.6 | peerDependencies pins this version, no wider range declared |
| Platform | macOS / Linux / Windows | Package itself declares no os/cpu restrictions |
| Native Modules | None | DSH integration package itself contains no native dependencies |
Installation
dsh plugin --profile web add github:strukto-ai/mirage/typescript/packages/dsh
Configuration Options
| Config | Type | Description | Default |
|---|---|---|---|
| MirageService.mounts | Object | Declarative mounts: each entry is a resource (registration name) / mode (read/write/exec) / config (resource config); if not passed, must pass a live workspace, one of two required | Not set |
| MirageService.workspace | Workspace | Mirage workspace with caller-owned lifecycle; after passing, plugin won't construct itself | Not set |
| MirageService.runtimes | String or Object Array | Runtime list (monty, pyodide, quickjs, etc.), can choose one with workspaceOptions.runtimes | Not set |
| MirageShellExecutor.workdir | String | Default working directory for commands; used as initial directory for that session when binding sessionId | / |
| MirageShellExecutor.sessionId | String | Bind to specified workspace session, all commands execute in that session, cd/export/function definitions persist across invocations | Not bound (each invocation independent subshell) |
| MirageShellExecutor.defaultTimeoutMs | Milliseconds | Default timeout for single command | 120000 |
| MirageShellExecutor.maxTimeoutMs | Milliseconds | Upper limit for caller-requested timeout | 600000 |
| MirageShellExecutor.stdoutMaxBytes | Bytes | stdout capture limit | 200000 |
| MirageShellExecutor.stderrMaxBytes | Bytes | stderr capture limit | 64000 |
| MirageShellExecutor.spillDir | String | "Spill" file directory for background command output overflow (should be workspace path like /tmp), Agent can read complete output via that path later | Not set (excess marked truncated, not written to disk) |
| MirageFileSystem.cwd | String | Virtual base directory for relative path resolution | / |
| MirageFileSystem.diffBasisMaxBytes | Bytes | "Before text" limit for before/after fields in write receipts | 10485760 (10 MiB) |
FAQ
Q: What content can dsh see by default after installation?
A: A /tmp is automatically mounted as a RAM temporary disk with exec permissions (scripts can be executed). Beyond that, there are no other real data sources; all read/write happens in memory for this session and disappears when dsh closes. When real data sources are needed, just override the mirage line in the profile.
Q: How to connect real data sources like Slack, Redis, S3?
A: In the profile's own cordis.patch.yml, change the mirage configuration to a mounts block, specifying resource (registration name like slack/redis/s3), mode (read/write/exec), and config (fields required by the resource). Use !!js process.env.SLACK_BOT_TOKEN to resolve secrets like tokens at load time, avoiding plaintext on disk.
Q: After enabling, why did dsh's built-in PowerShell and ripgrep search disappear?
A: These two tools spawn host processes and have no place in the workspace, so this bundle explicitly disables pwsh-sandbox, tool-pwsh, and tool-fs-search. Search capability is covered by grep in bash tools, covering all mount sources.
Q: How to keep bash tool's directory and variables across multiple invocations?
A: Pass the sessionId field to MirageShellExecutor (e.g., 'agent-1'), then cd, export, and function definitions persist across invocations; without it, each invocation is an independent subshell with no persistent state. The same sessionId means connecting to the same workspace session, different ones are completely isolated.
Q: Is the workspace after installation read-only or writable?
A: Depends on each mount's mode, with read < write < exec three levels progressively opening up. The default /tmp is exec level, allowing bash scripts to be executed. To let dsh's file tools write to a mount (e.g., writing results back to Redis), declare it as mode: write or mode: exec.
Q: How are high-output background commands handled?
A: By default stdout buffers 200KB and stderr buffers 64KB, excess is truncated and marked truncated. To preserve complete output, point MirageShellExecutor.spillDir to a workspace path (like /tmp), overflow portions are written to disk as files, paths returned in readOutput(), Agent can read them back via the same VFS.
Q: Is it an official DSH plugin?
A: It's not from the DSH repository. It inserts its three Cordis plugins into the dsh system via dsh's bundle mechanism (cordis.patch.yml), replacing dsh's ctx.fs and ctx.shell two capability seams. The underlying layer uses DSH 0.1.0-rc.6's exposed fs/shell plugin interfaces.
Q: How to mount my own backend?
A: On the caller side, registerResourceFactory('my-store', (config) => new MyStore(config)), then write resource: my-store in the mount block. Registration logic must complete before workspace construction, so put it in another profile-loaded plugin or import directly before MirageService starts.
Learning Curve
Advanced — The default /tmp works out of the box to run commands, but to let the Agent actually access Slack/Redis etc. data, you need to write mount blocks in the profile's cordis.patch.yml and understand the mode three levels and !!js expression semantics; enabling persistent shell, spill directory, Python sandbox, etc. also requires some configuration.
Known Issues and Limitations
- The bundle disables dsh's built-in PowerShell tools and full-text search tools based on ripgrep because they run host processes and have no place in the workspace; for search use
grepin bash tools instead - Write operations to the same target path are serialized (locked by path), with concurrent writes having only one winner, other requests observe the new version and are rejected as stale by the old version guard
- When the workspace includes a runtime that "bypasses VFS" (like host's local Python runtime), the shell plugin reports
sandboxMode = undefined(i.e., "no sandbox") to dsh instead of declaring it asworkspace-write; in this case dsh's permission presets won't combine with this shell - When
spillDiris not set, background command output exceeding stdout buffer is only markedtruncated, not written to file; to be "fully readable" must explicitly give a workspace path - Bash tools are isolated between invocations by default (state not persistent), to retain cwd/export/functions must pass
sessionId; different shell instances on the same sessionId share the same workspace session - Mount mode strength is ordered as
read < write < exec, scripts cannot execute on weak mode mounts; to let Agent run Python scripts need to explicitly declare/tmp(or corresponding path) asmode: exec - The "stale version" guard for write operations is based on backend's fingerprint or modification time + size, backends that cannot represent both simultaneously report as
unversioned, no version comparison on write - Reading binary files (first 8KB contains NUL bytes) or non-UTF-8 text is rejected by
decodeStrictTextasFS_NOT_TEXT; reading uses fatal UTF-8 decoding, no lenient replacement
Mirage is a Unified Virtual File System for AI Agents: it mounts services and data sources like S3, Google Drive, Slack, Gmail, and Redis side-by-side as one filesystem. Any LLM that already knows bash can read, grep, and pipe across every backend out of the box, with zero new vocabulary.
ws = Workspace(
{
"/tmp": (RAMResource(), MountMode.EXEC),
"/redis": (RedisResource(url=redis_url), MountMode.WRITE),
"/slack": (SlackResource(SlackConfig(token=slack_bot_token)), MountMode.EXEC),
},
# monty captures python, so scripts run sandboxed inside the workspace
runtimes=[MontyRuntime(captures=["python", "python3"]), "vfs"],
)
# one grep sweeps every source
await ws.execute("grep -rln session /redis /tmp")
# run a script that lives in Slack, file the report into Redis
await ws.execute(
"python3 /slack/channels/general__C0.../files/example__F0....py > /redis/report.txt"
)
# install a typed CLI under a head word: dispatched by name, not by path,
# and discoverable through `man`, `type` and `which` like any other program
ws.register_cli("slack", SLACK, {"token": slack_bot_token})
await ws.execute('slack send-message --channel general --text "report is up"')
About
- One interface instead of N SDKs and M MCPs. Every service speaks the same filesystem semantics, and pipelines compose across services as naturally as on a local disk.
- Around 50 built-in backends: RAM, Disk, Redis, S3 / R2 / OCI / Supabase / GCS, Gmail / GDrive / GDocs / GSheets / GSlides, GitHub / Linear / Notion / Trello, Slack / Discord / Email, MongoDB / GridFS / Postgres / LanceDB / Qdrant, SSH, and more, mounted side-by-side under a single root.
- Portable workspaces: clone, snapshot, and version a workspace; agent runs move between machines without restarting or reconfiguring the system.
- Embeddable: the Python and TypeScript SDKs run in-process inside FastAPI, Express, browser apps, or any async runtime; no separate process required.
- Agent integrations: OpenAI Agents SDK, Vercel AI SDK, LangChain, Pydantic AI, CAMEL, and OpenHands via the SDKs; coding agents through native adapters, installable plugins, MCP, or FUSE.
Architecture
Installation
- Python ≥ 3.11 for the
mirage-aipackage and themirageCLI - Node.js ≥ 20 for the TypeScript SDK
- macOS or Linux (FUSE-based mounts require platform support)
Python
uv add mirage-ai # installs the `mirage` library and the `mirage` CLI binary
TypeScript
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
Both runtime packages pull in @struktoai/mirage-core automatically.
CLI
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
Quickstart
Python
from mirage import Workspace
from mirage.resource.ram import RAMResource
from mirage.resource.s3 import S3Config, S3Resource
ws = Workspace({
"/data": RAMResource(),
"/s3": S3Resource(S3Config(bucket="my-bucket")),
})
await ws.execute("cp /s3/report.csv /data/report.csv")
await ws.execute("grep alert /s3/data/log.jsonl | wc -l")
await ws.snapshot("demo.tar")
TypeScript
import { Workspace, RAMResource, S3Resource } from '@struktoai/mirage-node'
const ws = new Workspace({
'/data': new RAMResource(),
'/s3': new S3Resource({ bucket: 'my-bucket' }),
})
await ws.execute('cp /s3/report.csv /data/report.csv')
await ws.execute('grep alert /s3/data/log.jsonl | wc -l')
await ws.snapshot('demo.tar')
CLI
mirage workspace create ws.yaml --id demo
mirage execute --workspace_id demo --command "cp /s3/report.csv /data/report.csv"
mirage provision --workspace_id demo --command "cat /s3/data/large.jsonl"
mirage workspace snapshot demo demo.tar
mirage workspace load demo.tar --id demo-restored
Agent Frameworks
Mirage plugs into agent frameworks as a sandbox or tool layer. POSIX operations such as read can also be customized per resource and filetype: Mirage ships no filetype renderers, so a format renders however you register it, and a command registered for one resource and extension wins over the generic one.
| Integrations | |
|---|---|
| Python | OpenAI Agents SDK, LangChain, Pydantic AI, CAMEL, OpenHands, Agno |
| TypeScript | Vercel AI SDK, OpenAI Agents SDK, LangChain, Mastra |
| Coding agents | Claude Code, Codex, DeepSeek Harness, Grok Build, OpenCode, Pi |
Cache
Every Workspace has a two-layer cache so repeated work against remote backends hits local state instead of the network:
- Index cache: listings and metadata. The first directory walk hits the API; later ones serve from the index until the TTL expires (default 10 minutes).
- File cache: object bytes. The first read streams from origin; later pipelines read from cache (default 512 MB).
Both layers default to in-process RAM with zero setup. A Redis store shares cache state across workers, processes, and machines:
import { RedisFileCacheStore, S3Resource, Workspace } from '@struktoai/mirage-node'
const ws = new Workspace(
{ '/s3': new S3Resource({ bucket: 'my-bucket' }) },
{
cache: new RedisFileCacheStore({ url: 'redis://localhost:6379/0', cacheLimit: '8GB' }),
index: { type: 'redis', url: 'redis://localhost:6379/0', ttl: 600 },
},
)
See the cache docs for the full miss/hit lifecycle.
Contributors
Thanks to everyone who has contributed to Mirage.