2-c-q
dsh-capsule
OS-isolated capability capsules for third-party DeepSeek Harness plugins
- Stars
- 0
- Language
- TypeScript
- Created
- Aug 15, 2026
- Updated
- Aug 15, 2026
Introduction
dsh-capsule
Everything is a plugin. Ambient authority should not be.
dsh-capsule v0.1 runs compatible third-party DeepSeek Harness guests in fresh Linux Bubblewrap processes. The trusted host verifies one integrity-pinned JavaScript artifact, discovers its contributions in an isolated describe cell, and registers real DSH tools and static system-prompt sections on the guest's behalf. Every tool call starts another fresh cell; guest code is never loaded into the DSH process and never remains resident between calls.
中文说明见 README.zh.md.
v0.1 security boundary
The v0.1 isolation provider is Linux-only and accepts a DSH managed subprocess runtime only after fail-closed execution-world attestation. Before any guest code runs, an empty-environment probe with bounded time and output must echo a private random challenge, report Host-identical Node, Bubblewrap, reported non-addon shared-object, mount-namespace, and root identities, then join its managed process tree. The built-in local subprocess provider is the normal implementation that satisfies this proof, but class identity is not required. The selected subprocess provider remains part of the trusted computing base.
A small trusted launcher opens every mounted runtime, artifact, and workspace source through stable descriptors without following a symbolic link, then Bubblewrap mounts only those descriptors. Bubblewrap constructs a new user, PID, mount, and network namespace from an empty root; adds the selected Node executable, non-addon shared libraries, the exact read-only guest artifact, a private bounded /tmp, /proc, a minimal /dev, and only explicitly admitted read-only workspace files; clears the inherited environment and sets only HOME=/tmp and PATH=/runtime; and drops capabilities. The strict launch document labels each mount runtime, guest, or workspace-read; Node workspace permissions derive only from workspace-read, not from a destination pathname. Node's Permission Model, disabled addons and inspector signal, V8 heap ceiling, protocol limits, deadlines, and the host scheduler provide defense in depth.
Node's Permission Model is not the isolation boundary. A configured capsule fails to activate on non-Linux hosts, when Bubblewrap is unavailable, when the artifact or policy does not match, or when any required control cannot be established. There is no cooperative provider and no silent downgrade.
Protocol v0.1 supports only exact workspace-read capabilities for existing regular files. Every request needs the same relative path in deployment grants; a requested workspace path needs an absolute workspaceRoot and must remain inside that root without symlink traversal. Bubblewrap mounts the file read-only at the corresponding /workspace/<path> destination and Node's allowlist mirrors it. A requested directory target or any workspace-write, network-connect, subprocess-exec, or storage request or grant fails activation. Empty requests and grants remain valid, as in the echo example. The threat claim is runtime authority containment within the documented Linux boundary, not protection from denial of service, kernel or Bubblewrap vulnerabilities, side channels, or unsafe prompt content. See the threat model.
Directory mounts are not a v0.1 capability because their contents can include Host IPC endpoints and other special files. Raw write mounts are also excluded because inode aliases can extend a write's effects beyond the granted pathname. A later directory or write capability must use a Host-owned broker or snapshot-and-commit mechanism instead of exposing either mount directly.
Install in DeepSeek Harness
Install Bubblewrap on a Debian or Ubuntu Linux host:
sudo apt-get update
sudo apt-get install --yes bubblewrap
Ubuntu 24.04 enables AppArmor restrictions on unprivileged user namespaces by default. If Capsule activation reports loopback: Failed RTM_NEWADDR: Operation not permitted, load Ubuntu's targeted Bubblewrap profile instead of disabling that restriction system-wide, then run the final command as a preflight:
sudo apt-get install --yes apparmor-profiles
sudo install -m 0644 \
/usr/share/apparmor/extra-profiles/bwrap-userns-restrict \
/etc/apparmor.d/bwrap-userns-restrict
sudo apparmor_parser --replace /etc/apparmor.d/bwrap-userns-restrict
bwrap --die-with-parent --new-session --unshare-all --unshare-user \
--disable-userns --cap-drop ALL --ro-bind / / /bin/true
The profile applies to every /usr/bin/bwrap invocation on that host, so an administrator should assess other Bubblewrap or Flatpak users before loading it on a long-lived machine. Other distributions need an equivalent administrator-approved policy that lets unprivileged Bubblewrap create its user, mount, PID, and network namespaces. Capsule does not change host AppArmor or sysctl policy itself.
The installed Bubblewrap must support --ro-bind-fd. The active managed subprocess provider must pass execution-world attestation; a different filesystem view, a different resolved object, a missing Bubblewrap feature, malformed or truncated proof output, timeout, or incomplete process-tree join fails activation. The host never selects pathname mounts or a weaker execution mode.
The package accepts @deepseek-ai/dsh-subprocess, @deepseek-ai/dsh-system-prompt, and @deepseek-ai/dsh-tools peers from 0.1.0-rc.5 up to but excluding 0.2.0. It does not require @deepseek-ai/dsh-subprocess-local as a package peer.
Install the bundle into the profile that should own the host:
dsh plugin --profile <profile> add github:2-c-q/dsh-capsule
The bundle mounts an inert dsh-capsule-host row with no capsules. Add a profile-level cordis.patch.yml override like the following, replacing the example root with the absolute path to this repository's examples/echo-capsule directory:
- id: dsh-capsule-host
config:
capsules:
- root: /absolute/path/to/dsh-capsule/examples/echo-capsule
manifest: capsule.json
policy:
capsuleId: example.echo
tools:
- capsule_echo
promptSections:
- capsule.echo.guidance
grants: []
bubblewrapCommand: bwrap
maxManifestBytes: 262144
maxArtifactBytes: 16777216
maxFrameBytes: 1048576
maxStderrBytes: 65536
maxDescriptionBytes: 4096
maxPromptBytes: 65536
maxContributions: 64
maxJsonNodes: 10000
maxJsonDepth: 32
startupTimeoutMs: 5000
callTimeoutMs: 30000
shutdownTimeoutMs: 500
processGraceMs: 500
maxConcurrentCalls: 4
maxQueuedCalls: 32
maxOldSpaceSizeMb: 128
tmpfsBytes: 16777216
A DSH patch replaces the target row's whole config; it does not deep-merge nested fields. An override must therefore restate every field it needs to preserve. The example explicitly repeats all v0.1 defaults so adding capsules does not discard a bundle-supplied limit. Use dsh --profile <profile> --dump-config to inspect the composed row before starting the profile.
To grant workspace access, add workspaceRoot beside root and manifest, then declare an exact read-only file in the guest manifest and deployment policy. For example, this manifest request:
{"kind":"workspace-read","path":"reference/context.txt"}
requires this capsule mount configuration:
workspaceRoot: /absolute/path/to/workspace
policy:
capsuleId: example.reader
tools:
- capsule_read
promptSections: []
grants:
- kind: workspace-read
path: reference/context.txt
/absolute/path/to/workspace/reference/context.txt must already be a regular file, must not traverse a symlink, and must remain disjoint from private Host staging. The guest sees it read-only at /workspace/reference/context.txt. A parent grant, a directory, a different permission kind, or an unsupported resource kind does not satisfy the request. Extra workspace-read policy entries are syntax-checked but grant nothing, require no filesystem target until requested, and are not mounted.
At activation, the host reads capsule.json without executing the guest, resolves the entry inside the configured root, verifies its SHA-256 digest, copies those exact bytes into a private read-only staging file, and requires every manifest contribution and resource request to fit the deployment policy upper bound. Extra policy entries grant nothing by themselves and are not mounted. The host stages immutable launcher bytes, opens each source by descriptor for the fresh describe cell, and publishes all admitted tools and prompt sections atomically through ctx.tools and ctx.systemPrompt. Tool calls then use the normal DSH validation, execution, presentation, and session-log path.
Build a guest
A guest imports the small SDK from dsh-capsule/guest and calls runCapsuleGuest(...). See examples/echo-capsule/source.ts for a complete tool and static prompt section.
The runtime mounts exactly one guest file and provides no node_modules, so bundle the SDK and every JavaScript dependency into one standalone ESM artifact:
pnpm exec esbuild examples/echo-capsule/source.ts \
--bundle \
--platform=node \
--format=esm \
--target=node22 \
--outfile=examples/echo-capsule/guest.mjs
sha256sum examples/echo-capsule/guest.mjs
Declare that lowercase digest in capsule.json. This echo guest requests no workspace access, so its resource array and policy grants are empty:
{
"protocol": "dsh-capsule/0",
"id": "example.echo",
"entry": "./guest.mjs",
"integrity": {
"sha256": "870a9cc8532a2b955ea5570ee9e8b79f6552029c403de95fd814f8489d5a5e58"
},
"contributes": {
"tools": ["capsule_echo"],
"promptSections": ["capsule.echo.guidance"]
},
"requests": []
}
Rebuild and update the digest whenever guest source, SDK code, or bundled dependencies change. Activation fails on an integrity mismatch; it never executes different bytes as a fallback. The example repository automates this with pnpm run build:example and checks it with pnpm run verify:example.
Protocol and limitations
Messages are strict, length-prefixed JSON with frame-byte limits; descriptor schemas additionally have depth and node-count limits. Time, concurrency, queue, stderr, prompt, and description limits are deployment-controlled. describe activates one fresh process only long enough to return immutable tool schemas and static prompt text. Each invocation activates a different process, revalidates the same descriptors, executes exactly one tool, completes an explicit shutdown handshake, and joins the managed process tree. Guest process state is therefore ephemeral. A read-only workspace file remains Host-owned and may change through trusted external activity, but the guest cannot use it to persist writes. Private persistent storage and all network or subprocess capabilities are unsupported in v0.1.
Capsule guests are a distinct API and are not transparent replacements for arbitrary Cordis plugins. Read the RFC for the exact protocol and lifecycle, and the dated ecosystem review for the project scope.
Development
pnpm install
pnpm run check
pnpm run build:example
pnpm run verify:example
pnpm run pack:check
License
MIT