dsh-remote-sandbox
Run a DeepSeek Harness turn inside a remote OpenSandbox container:
type /dsh-sandbox <message> and that message — and everything the agent does while answering it —
executes in an isolated Linux sandbox instead of on your machine. Files the sandbox produces are
copied back into your workspace automatically.
Nothing changes until you type the command. Other sessions, other turns, and every tool this plugin does not own keep their ordinary behaviour.
What it does
/dsh-sandbox <message> |
Attach a sandbox to this session, mirror the workspace into it, and answer the message there |
/dsh-sandbox on / off |
Enter or leave sandbox mode without sending a message |
/dsh-sandbox status |
Show the container, its state, and both mirror roots |
/dsh-sandbox sync |
Copy sandbox changes back into the workspace immediately |
/dsh-sandbox destroy |
Leave sandbox mode and delete the container |
/dsh-sandbox help |
Usage |
While a session is in sandbox mode:
pwshandbashcommands run inside the container, through its own shell (bash). The model is told to write POSIX commands rather than PowerShell cmdlets.read,write,edit,glob, andgrepread and write the container's copy of the workspace.- After every operation, new and modified files are copied back into the workspace, so generated
output lands where you expect it. Local copies of
write/editresults are updated immediately. - Tools that never touch the workspace (
web_search,web_fetch,todo_write,skill, subagent and job surfaces, …) keep running on the host and are unaffected.
Fail closed: nothing silently runs locally
While a session is in sandbox mode, a workspace-touching call either runs inside the sandbox or it fails. The failure is an ordinary tool error the model sees:
沙箱处理失败:<what failed>
本次调用没有在本地工作区执行任何操作。
There is no fallback path. Specifically:
| Situation | Behaviour |
|---|---|
| The container answers 502/503 (paused, restarting, proxy blip) | The client forgets the cached endpoint, asks the server to resume, retries up to 3 times; if it still fails, the call fails |
| The sandbox handle was lost (host restart) | The plugin reattaches to the session's sandbox by metadata; if that fails, the call fails |
| The tool is workspace-touching but not routed | Refused with SANDBOX_TOOL_UNSUPPORTED, never delegated |
| A command ran but its files could not be pulled out | The call fails and says so — a file left in the container was not delivered |
Local execution happens only when the session never invoked /dsh-sandbox. The mode flag — not
the presence of a live container — is what gates every call, so a lost handle can never quietly turn
sandboxed work into host work.
Verify it yourself: /dsh-sandbox status prints the server's reachability and probes the container
(container: usable — Linux 6.8.0-… x86_64, pwd /workspace). Inside a sandboxed turn, uname -s
reports Linux and /etc/os-release exists, while the host is Windows.
Install
The plugin is a DSH bundle: its cordis.patch.yml declares one row, and the package's lib/index.js
is the host plugin. There is no build step — the shipped files are the sources DSH loads.
From a tarball (what to hand to someone else)
# the recipient, inside their own profile:
dsh plugin --profile web add ./dsh-remote-sandbox-0.1.0.tgz
# then restart DSH once
npm pack / pnpm pack in this directory produces exactly that tarball; the files field keeps it
to lib/, cordis.patch.yml, README.md, and LICENSE.
With the dsh CLI, from a checkout
dsh plugin --profile web add link:/absolute/path/to/dsh-remote-sandbox
A linked package resolves through this checkout, so after any code change the plugin must be restarted: module roots are not watched.
By hand
- Make the package resolvable from the profile, e.g. a directory junction from
%DSH_HOME%\profiles\<profile>\node_modules\dsh-remote-sandboxto this checkout. - Add one row to the profile's
cordis.patch.yml:
- id: remote-sandbox
name: dsh-remote-sandbox
config:
baseUrl: 192.10.50.139:9999
apiKey: replace-with-a-random-secret
With HMR active the row loads immediately; otherwise restart DSH.
Configure
Two places, same schema:
- the row's
configin the profile patch (shown above), and - Plugins → dsh-remote-sandbox → Configuration in the Web client, which renders the plugin's
exported Schemastery
Configas a form and writes the override back into the profile patch.
| Field | Default | Meaning |
|---|---|---|
baseUrl |
192.10.50.139:9999 |
OpenSandbox server (host:port or an http(s):// URL) |
apiKey |
replace-with-a-random-secret |
Sent as OPEN-SANDBOX-API-KEY |
image |
ubuntu:24.04 |
Image used when a sandbox is created |
execdPort |
44772 |
Sandbox-side execd port, resolved through the lifecycle API |
sandboxTimeoutSeconds |
3600 |
Container lifetime; expired sandboxes are recreated on demand |
requestTimeoutMs |
60000 |
One lifecycle or file-transfer request |
commandTimeoutMs |
300000 |
Default command timeout inside the sandbox |
remoteWorkspace |
/workspace |
Directory that mirrors the session workspace |
syncOnStart |
true |
Upload the workspace when sandbox mode starts |
pullBack |
true |
Copy new/changed files back after every operation |
pullBackDeletes |
false |
Also delete local files the sandbox lost |
maxUploadBytes / maxPullBytes |
8388608 |
Per-file size caps for either direction |
exclude |
node_modules, .git, … |
Never uploaded |
pullBackExclude |
node_modules, .git |
Never copied back |
systemPrompt |
true |
Tell the model it is inside the sandbox |
How it works
- Activation.
/dsh-sandboxis a human command (ctx.commands.register), so the command line never reaches the model as text. The handler creates or reattaches a sandbox, then submits the remaining message withagent.steer()— an ordinary user message, so the normal turn runs. - Identity. Sandboxes carry metadata
dsh-session-id; a restart or a later/dsh-sandboxreattaches to the same container and its files. The server's own timeout reclaims stale ones. - Tool routing. The plugin wraps the registry's
tools/executewaterfall. For a session in sandbox mode it routespwsh,bash,read,write,edit,glob, andgrepto the container, returns a result built to each tool's ownoutput.schema, and refuses anything else that could touch the workspace. Only sessions with the mode flag set are affected. - Mirroring. The workspace is uploaded once (
find-based manifest + per-file upload), then each operation is followed by a manifest diff that downloads only what changed. The sandbox manifest is re-read after any upload, so a file is never uploaded and downloaded again in the same turn. - Model guidance. A system-prompt section (order 550, only while sandbox mode is on) explains the container, the path mapping, POSIX commands, the failure contract, and that files are copied back.
Handing it to someone else
- Pack it:
npm pack(orpnpm pack) in this directory →dsh-remote-sandbox-0.1.0.tgz. - They install it into their own profile:
dsh plugin --profile <profile> add ./dsh-remote-sandbox-0.1.0.tgz, then restart DSH once. - They set their own
baseUrlandapiKey— on Plugins → dsh-remote-sandbox → Configuration, or in their profile'scordis.patch.yml. The built-in defaults point at the documented test server and will not reach their environment. - They confirm the wiring with
/dsh-sandbox status: it prints whether the server answers, the container's state, and a live probe of the container.
Two notes for recipients:
@deepseek-ai/schemasteryis a peer dependency: the harness supplies it, and it is what renders the Configuration form. A profile that cannot resolve it will not show the form.- Nothing else is needed — no build step, no runtime dependencies, plain ESM. If they prefer a
checkout,
add link:<dir>works too, but then every code change needs a restart.
Requirements
- DSH with
ctx.commands,ctx.tools, andctx.systemPrompt(any base-backed profile),>= 0.2.x. - An OpenSandbox server reachable from the DSH host, with an image whose execd exposes the
/command,/files/*, and/directories*routes. - Node 22.19+ or 24+ (uses the global
fetch,FormData, andBlob).
Tests
# against a live server: create/list/exec/upload/download/delete, timeouts, exit codes
node test/smoke-opensandbox.mjs
# the fail-closed contract, with no server needed: every sandboxed call either runs
# remotely or fails, and never falls through to the host
node test/isolation.mjs
# the plugin under the real Cordis runtime with stand-in services
node test/cordis-load.mjs
# the whole plugin (command → tools → sync) with a stand-in host context
node test/e2e-plugin.mjs
The last three run offline except e2e-plugin.mjs, which drives a real sandbox; that one and the
smoke test read SANDBOX_URL and SANDBOX_KEY from the environment, defaulting to the documented
test environment.
Known limits
- Paths outside the workspace are refused in sandbox mode; only the workspace is mirrored.
- Commands run in the foreground;
run_in_backgroundis rejected with a clear message. read_imageis not routed, so it is refused in sandbox mode; images the container produces are copied back first, then can be read after leaving the mode.- Local edits made while sandbox mode is on are uploaded before the next remote command, but not pushed continuously. A failed upload warns instead of failing the command.
- Sandbox mode is bound to the session that ran the command. Subagents started from that session
have their own sessions and therefore still run on the host. If that matters, ask the model to do
the work in the main session rather than in a subagent, or set
systemPrompt: falseand instruct it yourself. - Sandboxes outlive the host on purpose (they are reattached by
dsh-session-idmetadata) and are reclaimed by the server'stimeoutunless/dsh-sandbox destroyremoves them. - A new bundle needs the host to recompose. With HMR active the row loads as soon as the profile changes; otherwise restart DSH once after installing. A linked package's code changes always need a restart, because module roots are not watched.
No comments yet. Be the first to write one.