Bundle
dsh-capsule
OS-isolated capability capsules for third-party DeepSeek Harness plugins
- Source
- 2-c-q
- stars
- 1 stars
- License
- MIT
- Updated
- Updated 5 days ago
Readme
# 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](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](docs/THREAT_MODEL.md).
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:
```sh
sudo apt-get update
sudo apt-get install --yes bubblewrap
```
Ubuntu 24.04 enables [AppArmor restrictions on unprivileged user namespaces](https://documentation.ubuntu.com/security/security-features/privilege-restriction/apparmor/) 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:
```sh
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:
```sh
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:
```yaml
- 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:
```json
{"kind":"workspace-read","path":"reference/context.txt"}
```
requires this capsule mount configuration:
```yaml
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`](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:
```sh
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:
```json
{
"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](docs/RFC.md) for the exact protocol and lifecycle, and the dated [ecosystem review](docs/ECOSYSTEM_REVIEW.md) for the project scope.
## Development
```sh
pnpm install
pnpm run check
pnpm run build:example
pnpm run verify:example
pnpm run pack:check
```
## License
MIT
Install
dsh plugin --profile web add github:2-c-q/dsh-capsule#c6e51383e5d2472bddb2c25693c62a402d5955e3
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-capsule from the hub
- This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.