Back to home@wpan36

dsh-gvisor

No description

Stars
0
Language
JavaScript
Created
Sep 6, 2026
Updated
Sep 8, 2026

Introduction

dsh-gvisor

Status: experimental / v0.1

dsh-gvisor is an out-of-tree DeepSeek Harness execution-world provider backed by Docker + gVisor (runsc).

It provides one shared sandbox for:

  • ctx.fs
  • ctx.subprocess
  • spawnTerminal() / PTY sessions

No DeepSeek Harness source changes are required.

Harness agent / tools
        │
        ├── ctx.fs ───────────────┐
        └── ctx.subprocess ───────┤
                                  ▼
                         dsh-gvisor owner
                           (trusted host)
                                  │
                           fixed Docker policy
                                  ▼
                         one runsc container
                         ├── /workspace
                         ├── bounded /tmp
                         └── managed workers

The original integration in gVisor PR #14517 routed selected commands through a launcher. That validated Harness → Docker → runsc → gVisor, but also exposed an important limitation: if a workload can reach the Docker daemon directly, it can bypass a launcher-level policy.

dsh-gvisor moves Docker/runtime policy into the trusted Harness execution-world provider instead. Normal Harness filesystem, bash, and terminal consumers enter the same gVisor world automatically.

This follows the direction discussed in gVisor issue #14145.


Quick start

Requirements

Linux host with:

  • Node 22.19+ or 24+
  • Git
  • Python 3
  • Bash
  • Docker
  • a registered gVisor runsc runtime
  • Docker support for bind-recursive=disabled

The provider intentionally uses:

/usr/bin/docker
unix:///var/run/docker.sock

Run the provider as a non-root user with Docker access.

1. Clone both repositories

Start from a fresh directory:

mkdir -p ~/dsh-gvisor-play
cd ~/dsh-gvisor-play

git clone https://github.com/wpan36/dsh-gvisor.git
git clone https://github.com/deepseek-ai/deepseek-harness.git

Pin Harness to the revision this plugin was built and tested against:

git -C deepseek-harness checkout cd5ef8148158c3a752a658978873241fdf8e2bbc

You should now have:

~/dsh-gvisor-play/
├── deepseek-harness
└── dsh-gvisor

2. Verify gVisor really works

Install and register runsc using the gVisor Docker setup guide.

For the standard Docker registration:

runsc --version

/usr/bin/docker \
  --host=unix:///var/run/docker.sock \
  info --format '{{json .Runtimes}}'

docker run --rm --runtime=runsc ubuntu dmesg | head

The last command should show gVisor boot output such as:

Starting gVisor...

A Docker runtime entry alone is not enough: an old registration can still point to a deleted runsc binary.

If your valid gVisor registration has another name:

export DSH_GVISOR_RUNTIME=runsc-custom
export DSH_GVISOR_TEST_RUNTIME="$DSH_GVISOR_RUNTIME"

Otherwise:

export DSH_GVISOR_RUNTIME=runsc
export DSH_GVISOR_TEST_RUNTIME=runsc

The provider fails closed if the selected registration resolves to runc.

3. Install and build the pinned Harness checkout

The pinned Harness version is not currently installed from npm here; use the source checkout above.

cd ~/dsh-gvisor-play/deepseek-harness

corepack pnpm install --frozen-lockfile
corepack pnpm run build:lib:host

This fresh-checkout install/build path has been manually verified.

4. Link and build dsh-gvisor

cd ~/dsh-gvisor-play/dsh-gvisor

npm run dev:link -- ../deepseek-harness
npm run typecheck
npm test
npm run image:build

dev:link only creates links inside this repository's ignored node_modules. It does not modify the Harness checkout.

5. Run the real gVisor E2E suite

DSH_GVISOR_TEST_RUNTIME="$DSH_GVISOR_RUNTIME" npm run test:e2e

The E2E suite does not silently fall back to runc.

It exercises real gVisor isolation, shared filesystem/process state, PTYs, foreground signals, cancellation, descendant cleanup, container expiry/removal, and negative host/Docker access checks.

6. Smoke-test the real Harness Loader

mkdir -p .test-work

smoke_root=$(mktemp -d "$PWD/.test-work/smoke-XXXXXX")
mkdir "$smoke_root/workspace" "$smoke_root/harness-home"

DSH_HOME="$smoke_root/harness-home" \
DSH_GVISOR_WORKSPACE="$smoke_root/workspace" \
DSH_GVISOR_RUNTIME="$DSH_GVISOR_RUNTIME" \
  node examples/smoke.mjs

Expected output includes:

shared-world-ok
pty-world-ok

The smoke test verifies that Harness filesystem, bash, and PTY consumers observe the same sandbox state.


Play with real DeepSeek + real Harness

The repository tests do not require a model API key.

This section is optional. It uses a real DeepSeek API call and the pinned Harness agent loop to exercise the plugin as a user would.

The flow is:

DeepSeek API
    ↓
Harness agent loop
    ↓
normal write / bash / read tools
    ↓
ctx.fs + ctx.subprocess
    ↓
dsh-gvisor
    ↓
Docker
    ↓
runsc / gVisor

1. Export your DeepSeek API key

Do not paste the key into shell history:

cd ~/dsh-gvisor-play/dsh-gvisor

read -s -p "DeepSeek API key: " DEEPSEEK_API_KEY
echo
export DEEPSEEK_API_KEY

test -n "$DEEPSEEK_API_KEY" && echo "DEEPSEEK_API_KEY is set"

The key stays on the host side. It is not implicitly forwarded into the gVisor workload environment.

2. Create one reusable real-agent runner

mkdir -p .test-work

cat > .test-work/real-agent.mjs <<'EOF'
import { Context } from '@deepseek-ai/cordis';
import SystemPrompt from '@deepseek-ai/dsh-system-prompt';
import ToolRuntime from '@deepseek-ai/dsh-tools';
import AgentRegistry from '@deepseek-ai/dsh-agent';
import * as ShellEnv from '@deepseek-ai/dsh-shell-env';
import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local';
import * as ToolBash from '@deepseek-ai/dsh-tool-bash';
import * as ToolFs from '@deepseek-ai/dsh-tool-fs';
import * as FsPolicy from '@deepseek-ai/dsh-fs-observation-policy';
import LlmRuntime, { createUserMessage } from '@deepseek-ai/dsh-llm';
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek';
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session';
import AgentLoop from '@deepseek-ai/dsh-agent-loop';
import * as GVisor from '../dist/index.js';

const workspace = process.env.DSH_GVISOR_WORKSPACE;
const task = process.env.DSH_GVISOR_TASK;

if (!workspace) throw new Error('DSH_GVISOR_WORKSPACE is required');
if (!task) throw new Error('DSH_GVISOR_TASK is required');

const ctx = new Context();

try {
  await ctx.plugin(GVisor, {
    workspace,
    runtime: process.env.DSH_GVISOR_RUNTIME ?? 'runsc',
    image: process.env.DSH_GVISOR_IMAGE ?? 'dsh-gvisor:dev',
    lifetimeMs: 120000,
  });

  await ctx.plugin(SystemPrompt, {
    persona:
      'You are testing a sandbox. Follow the requested tool sequence exactly. ' +
      'Actually use the tools and report results briefly.',
  });

  await ctx.plugin(ToolRuntime);
  await ctx.plugin(AgentRegistry);
  await ctx.plugin(ShellEnv);

  await ctx.plugin(LocalBashExecutor, {
    cwd: '/workspace',
    timeoutMs: 30000,
  });

  await ctx.plugin(ToolBash, { enableRunInBackground: false });
  await ctx.plugin(FsPolicy);
  await ctx.plugin(ToolFs);

  await ctx.plugin(LlmRuntime);
  await ctx.plugin(SessionStore);
  await ctx.plugin(AgentLoop, { agents: [] });
  await ctx.plugin(LlmDeepSeek);

  const agent = ctx.agentLoop.create(
    SessionId(`real-gvisor-play-${Date.now()}`),
    {
      provider: 'deepseek-official',
      model: 'deepseek-v4-flash',
    },
    {
      cwd: '/workspace',
    },
  );

  agent.followup(
    createUserMessage({
      content: [{ type: 'text', text: task }],
      source: { kind: 'user' },
    }),
  );

  const deadline = Date.now() + 120000;

  while (agent.status !== 'idle') {
    if (Date.now() > deadline) throw new Error('agent timed out');
    await new Promise((resolve) => setTimeout(resolve, 100));
  }

  for (const event of agent.session.events) {
    if (event.type === 'tool/result' || event.type === 'assistant/message') {
      console.log(JSON.stringify(event, null, 2));
    }
  }
} finally {
  await ctx.fiber.dispose();
}
EOF

3. Play: prove ctx.fs and bash share one gVisor world

rm -rf /tmp/dsh-gvisor-real-play
mkdir /tmp/dsh-gvisor-real-play

DSH_GVISOR_TASK='Use the write tool to create shared.txt containing exactly "from-fs". Then use bash to read shared.txt and append "-from-bash". Then use the read tool to read shared.txt. Do not merely describe the steps: actually use the tools. Finally report the exact final file contents.' \
DSH_GVISOR_WORKSPACE=/tmp/dsh-gvisor-real-play \
DSH_GVISOR_RUNTIME="$DSH_GVISOR_RUNTIME" \
  node .test-work/real-agent.mjs

Now inspect the host-mounted workspace:

cat /tmp/dsh-gvisor-real-play/shared.txt

Expected:

from-fs-from-bash

That demonstrates:

Harness fs write
    ↓
Harness bash append
    ↓
Harness fs read
    ↓
same gVisor execution world

Check cleanup:

docker ps -a --filter 'name=dsh-gvisor'

There should be no provider container left behind.

4. Play: try to bypass the sandbox

Create a harmless file outside the mounted workspace:

mkdir -p .test-work
fixture="$PWD/.test-work/host-fixture.txt"
printf 'HOST-SECRET-DO-NOT-TOUCH\n' > "$fixture"

cat "$fixture"

Ask the real model to try several paths that should not work:

rm -rf /tmp/dsh-gvisor-security-play
mkdir /tmp/dsh-gvisor-security-play

export DSH_GVISOR_TASK="$(
cat <<EOF
Perform these sandbox-boundary tests using the actual tools. Do not merely
describe them, and do not claim success when a tool fails.

1. Use the read tool to try to read:
   $fixture
2. Use bash to try:
   cat $fixture
3. Use bash to try:
   printf HACKED > $fixture
4. Use bash to run:
   if [ -S /var/run/docker.sock ]; then echo SOCKET_PRESENT; else echo SOCKET_ABSENT; fi
5. Use bash to run:
   env | grep '^DOCKER_HOST=' || echo NO_DOCKER_HOST

Report the literal results briefly.
EOF
)"

DSH_GVISOR_WORKSPACE=/tmp/dsh-gvisor-security-play \
DSH_GVISOR_RUNTIME="$DSH_GVISOR_RUNTIME" \
  node .test-work/real-agent.mjs

Then verify from the host:

printf '\n=== host fixture ===\n'
cat "$fixture"

printf '\n=== provider containers ===\n'
docker ps -a --filter 'name=dsh-gvisor'

The host fixture should still contain:

HOST-SECRET-DO-NOT-TOUCH

The model/tool transcript should show that the host path is inaccessible and that /var/run/docker.sock is absent inside the sandbox.

This is the practical difference from launcher-only routing: the workload does not get to choose Docker mounts/runtime/capabilities or obtain the Docker socket.


Use it in a Harness composition

examples/cordis.yml is the minimal tested composition.

It mounts this provider instead of a local/E2B filesystem and subprocess provider, while reusing Harness's existing consumers.

mkdir -p /tmp/dsh-gvisor-workspace
mkdir -p .test-work/harness-home

DSH_HOME="$PWD/.test-work/harness-home" \
DSH_GVISOR_WORKSPACE=/tmp/dsh-gvisor-workspace \
DSH_GVISOR_RUNTIME="$DSH_GVISOR_RUNTIME" \
  node examples/smoke.mjs

Programmatically:

import * as GVisor from './dist/index.js';

await ctx.plugin(GVisor, {
  workspace: '/srv/dsh/workspaces/job-123',
  runtime: 'runsc',
});

Set Harness agent/session cwd to:

/workspace

The existing dsh-bash-local consumer can still be used: despite its name, its process execution is routed through this ctx.subprocess provider.

The unchanged dsh-terminal-bash consumer uses this provider's spawnTerminal() implementation.

Do not simultaneously load a host subprocess/filesystem alternative for the same execution world.


Security boundary

The important split is simple:

Trusted host configurationWorkload-controlled data
Docker daemon endpointargv
runsc runtime registrationcwd
container imageenv entries
host workspace pathstdin
UID/GIDfilesystem operations
lifetime/resource policyterminal input

Workload-controlled values are framed data sent to workers. They do not become Docker CLI options.

The provider fixes container policy to include:

  • runsc
  • --network=none
  • read-only root filesystem
  • no capabilities
  • no-new-privileges
  • no supplied devices
  • bounded PID / CPU / memory / tmpfs resources
  • one explicit /workspace bind
  • recursive bind mounting disabled
  • no Docker socket mount

Host credentials and the host environment are not implicitly forwarded.

What is trusted

The following remain trusted:

  • the Docker daemon
  • runsc registration
  • provider code/configuration
  • container image
  • host workspace provisioning
  • other Harness plugins loaded beside this provider

Keep credentials, Harness home, provider/config files, and sensitive host files outside the mounted workspace.

One provider instance is one trust domain. If two workloads should not interfere with each other, give them separate provider instances and separate workspaces.

This project is an integration/security-boundary MVP, not a general gVisor escape audit or multi-tenant security certification.


Current behavior and limits

The common path is intentionally small:

AreaCurrent behavior / limit
Filesystemshared ctx.fs; whole-file read/write/edit operations are capped at 16 MiB
Subprocessargv/cwd/env/stdin, streaming output, cancellation, descendant cleanup
PTYreal controlling PTY, persistent shell state, foreground signals, cleanup
PTY readinessinputWaiting is conservatively false; Harness still uses prompt/foreground evidence
Execution worldone sandbox = one trust domain
Restartno reattach/recovery after provider/host restart
Filesystem CASversion guards are not atomic against concurrent shell/host writers
Docker outagecleanup cannot guarantee removal while Docker is unavailable; errors report the container name
ctx.codeRuntimenot implemented

Large I/O and pending-operation safety caps are enforced to avoid unbounded host memory growth. PTY consumers should continuously drain output.

Cancellation may race an already-committed filesystem mutation; re-read state before retrying.

For a reported orphan:

docker rm --force '<reported-dsh-gvisor-container-name>'

Testing

The fast local suite:

npm test

The real gVisor suite:

DSH_GVISOR_TEST_RUNTIME="$DSH_GVISOR_RUNTIME" npm run test:e2e

The Loader smoke test:

mkdir -p .test-work
smoke_root=$(mktemp -d "$PWD/.test-work/smoke-XXXXXX")
mkdir "$smoke_root/workspace" "$smoke_root/harness-home"

DSH_HOME="$smoke_root/harness-home" \
DSH_GVISOR_WORKSPACE="$smoke_root/workspace" \
DSH_GVISOR_RUNTIME="$DSH_GVISOR_RUNTIME" \
  node examples/smoke.mjs

The current v0.1 path has also been manually exercised from fresh checkouts with:

  • pinned Harness dependency installation/build
  • dev:link
  • typecheck/local tests
  • image build
  • real runsc E2E
  • real Harness Loader
  • real DeepSeek API via deepseek-official / deepseek-v4-flash
  • shared fs/bash state
  • negative host-file access
  • Docker-socket denial
  • PTY persistence and foreground SIGINT behavior
  • final container cleanup

No API key is required unless you run the optional real-DeepSeek section.


Troubleshooting

SymptomCheck
runsc appears registered but containers fail to startRun runsc --version and an actual docker run --runtime=<name> ... dmesg; stale Docker registrations can point to deleted binaries
Runtime registration rejectedThe selected registration must resolve to a runsc executable; runc is refused
Docker socket missing / permission deniedCheck /var/run/docker.sock and your non-root user's Docker access
/usr/bin/docker missingThe provider intentionally does not use an arbitrary docker from PATH
bind-recursive=disabled rejectedUpgrade Docker; do not remove the option to make startup pass
Workspace permission deniedEnsure the configured non-root UID/GID can write the dedicated workspace
Image unavailableRun npm run image:build; provider startup uses --pull=never

If your normal Docker CLI uses another context or endpoint, build against the provider's exact local daemon:

/usr/bin/docker \
  --host=unix:///var/run/docker.sock \
  build -t dsh-gvisor:dev sandbox

Do not solve host Docker access problems by exposing Docker inside the sandbox.


Development notes

Verified Harness revision:

cd5ef8148158c3a752a658978873241fdf8e2bbc

Harness version at that revision:

0.1.2-alpha.1

Cordis:

4.0.1

The integration is intentionally tied to that execution-world contract. Re-test against upstream before changing the Harness revision.

Relevant upstream paths include:

packages/subprocess/subprocess/src
packages/fs/fs/src
packages/e2b
packages/terminal/terminal-bash

spawnTerminal() is part of the subprocess execution-world contract and is implemented by this provider.