mirage/typescript/packages/dsh

3.5kStars259Forks107Issues9Watchers

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
agent-sandboxagent-toolsai-agentsbashclaude-codedshdsh-pluginfuse

Install

$ dsh plugin --profile web add github:strukto-ai/mirage/typescript/packages/dsh

Run 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.X at 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 sessionId to 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-fs and @deepseek-ai/dsh-shell (DSH's file and shell capability seams)
  • Architecture Pattern: Loaded via dsh's bundle mechanism (package.json's dsh.bundle.patch points 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 (MirageService holds the workspace, MirageFileSystem and MirageShellExecutor each declare inject = ['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

DependencyMinimum VersionDescription
Node.js>=20.10.0Declared by package's engines field
DSH0.1.0-rc.6peerDependencies pins this version, no wider range declared
PlatformmacOS / Linux / WindowsPackage itself declares no os/cpu restrictions
Native ModulesNoneDSH integration package itself contains no native dependencies

Installation

dsh plugin --profile web add github:strukto-ai/mirage/typescript/packages/dsh

Configuration Options

ConfigTypeDescriptionDefault
MirageService.mountsObjectDeclarative 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 requiredNot set
MirageService.workspaceWorkspaceMirage workspace with caller-owned lifecycle; after passing, plugin won't construct itselfNot set
MirageService.runtimesString or Object ArrayRuntime list (monty, pyodide, quickjs, etc.), can choose one with workspaceOptions.runtimesNot set
MirageShellExecutor.workdirStringDefault working directory for commands; used as initial directory for that session when binding sessionId/
MirageShellExecutor.sessionIdStringBind to specified workspace session, all commands execute in that session, cd/export/function definitions persist across invocationsNot bound (each invocation independent subshell)
MirageShellExecutor.defaultTimeoutMsMillisecondsDefault timeout for single command120000
MirageShellExecutor.maxTimeoutMsMillisecondsUpper limit for caller-requested timeout600000
MirageShellExecutor.stdoutMaxBytesBytesstdout capture limit200000
MirageShellExecutor.stderrMaxBytesBytesstderr capture limit64000
MirageShellExecutor.spillDirString"Spill" file directory for background command output overflow (should be workspace path like /tmp), Agent can read complete output via that path laterNot set (excess marked truncated, not written to disk)
MirageFileSystem.cwdStringVirtual base directory for relative path resolution/
MirageFileSystem.diffBasisMaxBytesBytes"Before text" limit for before/after fields in write receipts10485760 (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 grep in 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 as workspace-write; in this case dsh's permission presets won't combine with this shell
  • When spillDir is not set, background command output exceeding stdout buffer is only marked truncated, 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) as mode: 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 decodeStrictText as FS_NOT_TEXT; reading uses fatal UTF-8 decoding, no lenient replacement