Skip to main content
DeepSeek Harness (dsh) is a plugin-based agent harness on Cordis where the filesystem and the shell are swappable capability seams. @struktoai/mirage-dsh provides both seams over one Mirage workspace: ctx.fs for dsh’s read/write/edit tools and ctx.shell for its bash tool. Anything Mirage mounts (S3, Gmail, Slack, Notion, Postgres) becomes the world those tools operate in.

Install

Compose

Three Cordis plugins. MirageService owns the workspace; the two providers inject it. Mix any resources, such as Slack and Redis beside a scratch RAM mount:
Mirage mounts are read-only by default; the mode ladder is READ < WRITE < EXEC, so mount with MountMode.WRITE where dsh’s write tools should work and MountMode.EXEC where scripts may run. dsh’s bash tool now spans every source in one line (resolve fills a request into a spec with the defaults and run executes it, the same two calls dsh’s own tools make):
MirageService also accepts mounts instead of a live workspace; it then constructs the workspace itself and closes it when the plugin unloads. Each command is a clean slate: cd, export, and function definitions inside one bash call do not survive into the next, which is the one-shot contract of dsh’s bash tool. For a persistent shell instead, bind the executor to a named workspace session:
A bound session keeps its cwd, exports, and functions across calls. It is created on first use (an existing session is adopted as is), and two executors bound to different sessions stay fully apart on one workspace.

Run Python with monty

The runtimes entry above sets monty, a sandboxed Python interpreter with no host access, up to capture python and python3; the catch-all vfs runtime serving the shell commands is always present and needs no entry. A captured invocation (a script file, inline -c code, or code piped on stdin) runs inside the workspace, never on the machine. A script can live on any mount: upload example.py to a Slack channel and run it straight off the mount, with the shell’s redirection filing its stdout back into Redis:
examples/typescript/dsh/dsh.ts is the runnable version of this whole page, with the script in example.py.

Install into a dsh profile

The package is also a dsh bundle: its manifest carries a patch layer that swaps dsh’s filesystem and bash providers for the mirage ones and disables the host-subprocess surfaces the workspace does not contain (PowerShell, the ripgrep search tool). Stock dsh, including its web app, then runs on mounted data:
The bundle’s default world is one RAM scratch mount at /tmp. Mount real resources by overriding the mirage row in the profile’s own cordis.patch.yml, in declarative form (the resource registry name, a mode, and the resource’s config; !!js expressions resolve at mount time):
The same blocks work in code, beside live instances, in MirageService’s mounts. The shell executor reports a workspace-write sandbox to dsh whenever every runtime in the world stays inside the VFS (each runtime’s reach is vfs), which is what lets dsh’s permission presets compose over it: a command cannot then reach anything but the mounts, under their modes. Adding a host-reaching runtime such as local python drops the claim, since a script could act outside the mounts, and dsh is told there is no sandbox rather than a false one.

Custom backends

A mount is not limited to the builtin resources (ram, s3, slack, redis, …). Register your own resource factory host-side and its name becomes usable in a mounts block exactly like a builtin:
The registration must run before the workspace builds, since a mounts block only names a resource, it does not construct one. In a dsh bundle that means a small plugin the profile loads alongside @struktoai/mirage-dsh (or a plain import in code, before MirageService starts), not the declarative patch. A builtin name cannot be shadowed, so a custom backend needs its own name. Nothing else changes: the shell, the commands, and the sandbox claim treat a custom mount like any other.

Background commands

A backgrounded command streams its output through a workspace console rather than arriving whole: each statement of a compound line lands as it finishes, and stdout and stderr keep their own channels. The unread backlog is bounded to the stdout budget, so a reader that never drains cannot grow it without limit; output past the budget is flagged lossy, with the freshest kept. Set spillDir on MirageShellExecutor to a workspace path (for example /tmp on a ram mount) and an overrunning command’s full stdout and stderr are written there, so the agent can read the complete output back through the same VFS; unset, nothing spills. A concrete scenario: serving agents from a TypeScript server (an Express endpoint, say), where every request is assigned its own context and workspace, created on entry and gone with the response. What one 2 vCPU / 8 GB box holds then depends on the harness: Concurrency is estimated as about 6 GB of free memory divided by a session’s measured resident footprint (a dsh session with a live Python engine is about 27 MB, idle workspaces 240 KB, a Codex session about 200 MB, a Claude Code CLI session about 430 MB); startup is the measured time to bring one session up, process boot for the CLIs and workspace construction for dsh. The dsh numbers already include isolation, since monty is the sandbox and each engine runs on its own crash-isolated worker process; the CLI sessions run on the host, so isolating them means provisioning a container or remote sandbox per session, measured at about 150 ms even for a warm local container and more for a cold or remote one.