Replace DeepSeek Harness files and shell backend with the Mirage workspace, enabling dsh file tools and bash to directly operate on mounted resources like S3, Slack, and Redis.
- Language
- TypeScript
- License
- Apache-2.0
- Branch
- main
Install
$ dsh plugin --profile web add github:strukto-ai/mirageRun 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 Pitch
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 file system interface (ctx.fs): read, write, edit, stat, listDir, etc. all go through the Mirage workspace, with all operations passing through the mount point mode and policy layer first
- Take over dsh's shell interface (ctx.shell): bash tool is changed to execute Mirage's shell, with POSIX tools (grep, ls, cat, cp, etc.) uniformly available across multiple mount sources
- Provide declarative mounting: list resources in profile's cordis.patch.yml (resource registration name, mode, config), with secrets like tokens parsed at mount time through
!!js process.env.X - Support sandboxed Python (monty) and about 50 built-in backends (RAM, Disk, S3, Redis, Slack, Gmail, GDrive, Notion, Postgres, SSH, etc.), scripts can run under any mount path
- Default mount a
/tmp(RAM disk, exec mode), usable for temporary files and Python script outputs - Support persistent shell: passing
sessionIdto the shell plugin preserves cd, export, and function definitions across multiple invocations - When all runtimes in the workspace are confined to the vfs, automatically report
workspace-writesandbox mode to dsh, allowing dsh's permission presets to combine with it
Technical Implementation
- Language: TypeScript (DSH integration package); the underlying Mirage has a separate Python implementation, this plugin only interacts with the TypeScript mirror
- 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 gaps, 0.1.0-rc.6) - Architecture Pattern: Loaded via dsh's bundle mechanism (
dsh.bundle.patchin package.json points to cordis.patch.yml); the patch disables dsh's built-in five plugins (fs-sandbox,bash-sandbox,pwsh-sandbox,tool-pwsh,tool-fs-search) and inserts mirage's trio (MirageServiceholds the workspace,MirageFileSystemandMirageShellExecutoreach declareinject = ['mirage']to get it), ultimately letting ctx.fs and ctx.shell share the same workspace - Entry Files:
typescript/packages/dsh/cordis.patch.yml(DSH load entry),typescript/packages/dsh/src/plugin/{service,fs,shell}.ts(each plugin's default export)
Use Cases
You want the Agent in dsh to operate not on the host machine's original file system, but on existing objects in external data sources like S3, Slack, Notion, Redis; or you want to give an Agent session a clean "virtual workspace" isolated from the host machine, where the Agent can read/write, run Python scripts, and produce files synchronously. Also suitable for directly embedding existing Python+TypeScript Mirage workspaces into the DSH system.
Prerequisites and Compatibility
| Dependency | Minimum Version | Notes |
|---|---|---|
| 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; underlying may need platform support if using FUSE real mounts |
| Native Modules | None | DSH integration package itself contains no native dependencies, runtime pulls mirage-node's dependencies on demand |
Installation
dsh plugin --profile web add github:strukto-ai/mirage
Configuration Options
| Config | Type | Description | Default |
|---|---|---|---|
| MirageService.mounts | Object | Declarative mounting: each entry is a resource (registration name) / mode (read/write/exec) / config (resource config); if not passed, must pass a live workspace, mutually exclusive | Not set |
| MirageService.workspace | Workspace | Mirage workspace with caller-owned lifecycle; once passed, plugin won't construct its own | Not set |
| MirageService.runtimes | String or Object Array | Runtime list (monty, pyodide, quickjs, etc.), can use either this or workspaceOptions.runtimes | Not set |
| MirageShellExecutor.workdir | String | Default working directory for commands; when binding sessionId, used as the initial directory for that session | / |
| MirageShellExecutor.sessionId | String | Bind to specified workspace session, all commands execute within that session, cd/export/function definitions persist across invocations | Not bound (each invocation is an 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" directory for background command output overflow (should be a workspace path like /tmp), Agent can subsequently read full output via this path | Not set (overflows are marked truncated instead of written to disk) |
| MirageFileSystem.cwd | String | Virtual base directory for relative path resolution | / |
| MirageFileSystem.diffBasisMaxBytes | Bytes | "Pre-text" limit for before/after fields in write receipts | 10485760 (10 MiB) |
FAQ
Q: What content does dsh see by default after installation?
A: A /tmp is automatically mounted as a RAM temp disk with exec (script execution) permission. Beyond that there are no other real data sources; all read/write happens in this session's memory and disappears when dsh is closed. When you need real data sources, just override the mirage line in your profile.
Q: How to connect real data sources like Slack, Redis, S3?
A: In your profile's cordis.patch.yml, change the mirage config 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 parse secrets at load time, avoiding plaintext on disk.
Q: After enabling, why are dsh's built-in PowerShell and ripgrep search gone?
A: These 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 the bash tool, spanning all mount sources.
Q: How to keep directory and variables persistent across multiple bash invocations?
A: Pass the sessionId field to MirageShellExecutor (e.g., 'agent-1'), then cd, export, and function definitions persist across invocations; if not passed, each invocation is an independent subshell with no persistent state. 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. Default /tmp is exec level, can execute bash scripts. To let dsh's file tools write to a mount (e.g., write results back to Redis), declare it as mode: write or mode: exec.
Q: How are large-output commands handled?
A: Default stdout buffers 200KB, stderr buffers 64KB, overflow is truncated and marked truncated. To preserve full output, point MirageShellExecutor.spillDir to a workspace path (like /tmp), overflow parts will be written to files, with paths returned in readOutput(), Agent can read back via the same VFS.
Q: Is it an official DSH plugin?
A: Not part of 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 gaps. The underlying 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 in the mount block write resource: my-store. Registration logic must complete before workspace construction, so put it in another plugin loaded by the profile (or import directly before MirageService starts in code). Built-in names cannot be overridden.
Learning Curve
Advanced — the default /tmp works out of the box to run commands, but to give the Agent real access to Slack/Redis etc., you need to write mount blocks in profile's cordis.patch.yml, figure out the three mode levels and !!js expression semantics; enabling persistent shell, spill directory, Python sandbox, etc. all require some configuration.
Known Issues and Limitations
- Bundle disables dsh's built-in PowerShell tools and full-text search tools based on ripgrep by default, because they run host processes and there's no place for them in the workspace; use
grepin bash tool for search instead - Write operations to the same target path are serialized (locked by path), when concurrent writes occur only one wins, other requests will observe the new version and be rejected by the stale version guard as outdated
- When the workspace includes a runtime that "bypasses VFS" (like host local Python runtime),
vfsOnlyreturns false, the shell plugin reportssandboxMode = undefined(i.e., "no sandbox") to dsh instead of declaringworkspace-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" you must explicitly give a workspace path - Bash tool is isolated between invocations by default (state not persistent), to preserve cwd/export/functions you 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 weakly mode'd mounts; to let Agent run Python scripts, 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 can't represent both simultaneously report as
unversioned, writes don't do version comparison
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.