Development & Runtime

dsh-plugin-multi-root-workspace

cherrchen/dsh-plugin-multi-root-workspace · ★ 24

cherrchen

Multi-root Workspace for DeepSeek Harness: adds N additional roots to the workspace sandbox scope as an out-of-tree bundle

License
MIT
Added
2026-10-30
Maintenance
active
Install kind
npm
npm
@dsh-electron/dsh-plugin-multi-root-workspace
Manifest details
Manifest status
Declared
Manifest path
package.json#dsh
Manifest checked
2026-10-30T09:39:27.418Z
Lifecycle scripts
None found

Static manifest checks report declarations only; they are not compatibility or security review.

安装

dsh plugin --profile web add '@dsh-electron/dsh-plugin-multi-root-workspace@0.1.11'

从 npm 安装预构建包

For install and environment setup, plus plugin development tutorials, see DSH community docs ↗

README

Source README · body: en · Read Chinese README

Table of contents
  1. dsh-plugin-multi-root-workspace
  2. What & Why
  3. Quick Start
  4. Requirements
  5. Installation
  6. Install from npm (recommended)
  7. Install from a tarball
  8. Install from GitHub
  9. Install from a local clone
  10. Running
  11. Usage
  12. Command
  13. Additional-root Skills
  14. Panel
  15. Model side
  16. Optional Auto Review integration
  17. Project Structure
  18. Contributing
  19. Known Limitations
  20. Documentation
  21. License

dsh-plugin-multi-root-workspace

中文 | English

What & Why

By default, DeepSeek Harness treats the directory a new workspace is created in as the agent's primary directory; under the default workspace read-write mode (workspace-write), the sandbox's read-write scope covers that directory alone. This plugin extends DSH's workspace scope to "one primary root + N additional roots".

Not every project is a monorepo. A real development project is usually several independent Git repositories, and working across them meant juggling sessions. Peer harnesses already support adding multiple folders to a single workspace. This plugin brings the same capability to DSH — register the other repositories as additional roots, grant each read-only, read-write, or unrestricted access, and the agent can work in them within that grant.

  • No new tools: the agent keeps using the native read / write / edit / bash; only the granted directories grow.
  • Sandbox confinement: an additional root may inherit the session's (primary root's) permission setting, or be pinned to read-only or read-write; under read-only and read-write, confinement does not rest on system prompts — it is enforced by an in-process filesystem fence plus kernel-level sandboxes (macOS Seatbelt / Linux bwrap / Landlock / Windows ACL), with files and bash sharing one and the same scope.
  • Browse additional roots in the Files tab: the Files tab header switches between the primary root and any registered additional root; an additional root's directory renders in the same native Files view, with open-file, open-in-app, and auto-refresh behaving identically.
  • Optional Auto review (off by default): a fork of upstream's experimental auto-review that gives the reviewer the additional directories' context — the current workspace topology, each root's access level, and live instruction files; it ships disabled and only takes effect once the @dsh-electron/dsh-plugin-multi-root-workspace/auto-review row is explicitly enabled in the Plugin Manager row list, taking over the official bundle while on.
  • Additional-root Skills: discovers .dsh/skills and .agents/skills inside additional folders, invoked through the namespaced /alias-id:skill-name reference and the native skill tool; each root has an independent toggle and editable aliasId, with a global setting that defaults to on.
  • Absent when absent: with no additional root configured, its behavior is item-for-item identical to an uninstalled harness; every misconfiguration fails loudly instead of silently degrading.

Current released version: v0.1.12. The user-visible changes of each version live in the CHANGELOG; progress and release state live in the roadmap progress ledger.

Quick Start

The plugin is published on npm. Open the Plugins page in the sidebar, choose Add plugin, and paste the package name @dsh-electron/dsh-plugin-multi-root-workspace — or install it in two commands:

dsh plugin --profile web add @dsh-electron/dsh-plugin-multi-root-workspace
dsh --profile web       # pick the --profile for how you launch DSH; see its docs for the values

Once it is up, the Folders action (🗂) appears at the sidebar foot — or, straight in a session:

/workspace-folders add ~/code/another-repo [--inherit|--read-only|--read-write]

The agent can now read, write, and run bash in that directory — the default is inherit, so the access level follows the session mode. Other install options (tarball, GitHub, a local clone) are covered under Installation.

Requirements

Using the plugin: a DSH runtime at exactly one of the versions below — an exact match, not a range. dsh plugin handles the install; no local Node toolchain is needed.

DSH line Supported releases
0.1.5 0.1.5-rc.2
0.1.6 0.1.6-alpha.1, 0.1.6-alpha.2
0.1.7 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2
0.2.0 0.2.0-rc.1, 0.2.0-rc.2
0.2.1 0.2.1-alpha.1, 0.2.1-alpha.2, 0.2.1-alpha.3, 0.2.1-alpha.4, 0.2.1-alpha.5, 0.2.1-rc.1, 0.2.1-rc.2, 0.2.1-rc.3, 0.2.1-rc.4, 0.2.1-rc.5
0.2.2 0.2.2-alpha.2

The plugin does not start — as if it were not installed — when the DSH release is not in the table or the @deepseek-ai/dsh-* packages disagree on a version.

Building from source / contributing: Node.js ^22.19.0 || >=24, pnpm 11.25.0 (enable via corepack); the dev-pinned DSH is 0.2.2-alpha.2 (Node ≥24.9).

Platforms: macOS (Seatbelt), Linux (bwrap / Landlock), Windows (root-set ACL runner).

Installation

The same install is available without the CLI: open Plugins in the sidebar, choose Add plugin, and enter the npm package name (or a Git address, tarball, or absolute local path).

dsh plugin supports four install sources. All commands below use the web profile as the example; for the Electron desktop swap --profile web for --profile desktop — the install method is the same.

Source Command Notes
npm registry (recommended) dsh plugin --profile web add @dsh-electron/dsh-plugin-multi-root-workspace Pre-built artifacts, ready to use
GitHub dsh plugin --profile web add github:cherrchen/dsh-plugin-multi-root-workspace Builds automatically from source on install; allowBuilds must name this package too — pin a version
tarball dsh plugin --profile web add ./dsh-electron-dsh-plugin-multi-root-workspace-<version>.tgz Pre-built offline package, handy for air-gapped or offline delivery
From source dsh plugin --profile web add /path/to/package/dsh-plugin-multi-root-workspace pnpm link: to a local checkout — best for development; no allowBuilds needed

The first dsh plugin add fails with [ERR_PNPM_IGNORED_BUILDS]: add the following line to that profile's pnpm-workspace.yaml, then run add again:

allowBuilds:
  koffi: true

If the retry exits 0 but the plugin never lands in dsh.profile.bundles, do a remove + add. Further diagnosis: troubleshooting.

dsh plugin --profile web add @dsh-electron/dsh-plugin-multi-root-workspace

This installs pre-built artifacts; the first install needs the allowBuilds addition described above.

Install from a tarball

pnpm pack @dsh-electron/dsh-plugin-multi-root-workspace
# or download the tgz from the GitHub Release assets, e.g.:
# https://github.com/cherrchen/dsh-plugin-multi-root-workspace/releases/download/v0.1.12/dsh-electron-dsh-plugin-multi-root-workspace-0.1.12.tgz
dsh plugin --profile web add ./dsh-electron-dsh-plugin-multi-root-workspace-0.1.12.tgz

Also pre-built, handy for air-gapped or offline delivery; the first install needs the same allowBuilds addition.

Install from GitHub

dsh plugin --profile web add github:cherrchen/dsh-plugin-multi-root-workspace

A GitHub install fetches source code and builds it automatically during add — no manual build needed — but allowBuilds has to cover two entries, this package itself and koffi:

allowBuilds:
  '@dsh-electron/dsh-plugin-multi-root-workspace': true
  koffi: true

Pinning a version (e.g. #v0.1.12) is recommended so a later push cannot silently change what actually runs:

dsh plugin --profile web add github:cherrchen/dsh-plugin-multi-root-workspace#v0.1.12

Install from a local clone

git clone https://github.com/cherrchen/dsh-plugin-multi-root-workspace.git
cd dsh-plugin-multi-root-workspace
export CI=true
pnpm install
pnpm build
dsh plugin --profile web add /path/to/repo

[!TIP] If dsh is not on your PATH (e.g. you run DSH from a source checkout), use pnpm dsh instead.

Running

Development gates:

pnpm lint && pnpm typecheck
pnpm test
pnpm kernel:probe
pnpm smoke
pnpm docs:check

Install into a DSH runtime and verify the composition:

dsh plugin --profile web add /path/to/repo
dsh --profile web --dump-config | grep -A3 'multi-root'

Expected output:

# == @deepseek-ai/dsh-base, patched by @dsh-electron/dsh-plugin-multi-root-workspace
- id: sandbox
  name: '@deepseek-ai/dsh-sandbox-local'
  disabled: true
--
# == @deepseek-ai/dsh-base, patched by @dsh-electron/dsh-plugin-multi-root-workspace
- id: fs-sandbox
  name: '@deepseek-ai/dsh-fs-sandbox'
  disabled: true
--
# == @dsh-electron/dsh-plugin-multi-root-workspace
- id: multi-root-compat
  name: '@dsh-electron/dsh-plugin-multi-root-workspace/compat'
- id: multi-root-fs
  name: '@dsh-electron/dsh-plugin-multi-root-workspace/fs'
- id: multi-root-sandbox
  name: '@dsh-electron/dsh-plugin-multi-root-workspace/sandbox'
- id: multi-root-providers
  name: '@dsh-electron/dsh-plugin-multi-root-workspace/providers'
- id: multi-root-scope
  name: '@dsh-electron/dsh-plugin-multi-root-workspace/scope'
  config:
    toolAccess: {}
- id: multi-root-registry
  name: '@dsh-electron/dsh-plugin-multi-root-workspace/registry'
  config:
    sharedStorageRoot: !!js dshHomePath('storages')
    leasePath: !!js dshHomePath('storages/multi_root_workspace.lock')
    maxAdditionalRoots: 16
- id: multi-root-instructions
  name: '@dsh-electron/dsh-plugin-multi-root-workspace/instructions'
  config:
    maxBytes: 65536
- id: multi-root-lsp
  name: '@dsh-electron/dsh-plugin-multi-root-workspace/lsp'
- id: multi-root-workspace-files
  name: '@dsh-electron/dsh-plugin-multi-root-workspace/workspace-files'
- id: multi-root-command
  name: '@dsh-electron/dsh-plugin-multi-root-workspace/command'
- id: multi-root-client
  name: '@dsh-electron/dsh-plugin-multi-root-workspace'

The upstream fs-sandbox and sandbox rows are disabled and taken over by the plugin's same-named services, and all the plugin's multi-root-* rows are mounted. After startup, a Folders action appears at the sidebar foot.

Usage

Command

/workspace-folders [list]                                                    # list the primary root and the additional roots (access and state included)
/workspace-folders add <absolute path> [--inherit|--read-only|--read-write]  # no path opens the OS directory picker; new registrations default to inherit
/workspace-folders access <n|path> inherit|read-only|read-write              # switch access (widening requires the root to be available right now)
/workspace-folders skills <n|path> on|off                                    # toggle additional-root Skills independently of write access
/workspace-folders alias-id <n|path> <kebab-case>                             # edit the stable Skill namespace
/workspace-folders alias <n|path> [name]                                     # omitting the name clears the alias
/workspace-folders remove <n|path>                                           # n is the number list prints
/workspace-folders reveal <n|path>                                           # show it in the file manager
/workspace-folders isolate-profiles [--apply]                                # inspect profile import method and multi-link files under $DSH_HOME; --apply writes copy config
/workspace-folders help                                                      # show the command help

Paths may be quoted; a path that duplicates an existing root, nests inside one, or is the workspace root itself is rejected.

Access is three-state (ADR-0017; the usage guide has the exact semantics): inherit follows the session mode — writable under workspace-write, refused under read-only, unrestricted under danger-full-access; read-only refuses writes in every mode, including Full-access; a fixed read-write root stays writable even in a read-only session. Writable records registered by older plugin versions read back as inherit. A primary root accepts at most 16 additional roots (maxAdditionalRoots can raise it to 64). A host may also cap a tool for all additional roots via toolAccess on the multi-root-scope row — e.g. { bash: 'read-only' } keeps bash from ever writing an additional root (it can still write the primary root).

After an add, list prints (a workspace-write session, with website registered read-only):

Workspace root (primary; access follows the current sandbox mode): /home/me/monorepo
  1 /home/me/payments-service [payments] (inherit)
  2 /home/me/website (read-only)
2 of 16 additional roots.
Writable additional roots under the current mode: 1 of 2.

Additional-root Skills

Skills are discovered in each additional folder’s .dsh/skills and .agents/skills. Names and descriptions are available before the first model step; bodies are loaded on invocation. The primary root uses /skill-name; additional roots always use /alias-id:skill-name, with Slash completion and manual entry. The model uses the native skill tool with an internal name. Internal IDs can appear in tool logs; human menus list namespaced references.

New inherit and read-write registrations enable Skills by default; read-only registrations disable them. The Workspace Folders dialog provides an independent Skills toggle and editable aliasId. Renaming the display Alias keeps aliasId stable. An actual change to registered access resets the toggle; session sandbox mode changes, same-value access updates, and re-registration without an access change preserve the manual choice. Enabling Skills on a read-only root grants no write access.

Enable additional-root Skills under Settings → Plugins → Multi-root Workspace controls all additional roots and defaults to on. Disabling it stops new discovery and invocation while preserving each root’s choice. Re-enabling restores discovery according to those choices. Primary-root Skills, instructions, LSP, browsing, and sandbox permissions retain their behavior. Older hosts use the compatible plugin settings page. Namespaced references appear as ordinary user text, without a separate background, highlighting, or click-to-preview action. Native draft highlighting does not yet support colon references.

On upgrade, legacy inherit and read-write registrations start with Skills enabled. Migration preserves existing grants. An older plugin can strip aliasId and manual toggles when it writes the registry; upgrading again applies the defaults. Lossless downgrade of Skills preferences is not guaranteed. Back up $DSH_HOME/storages/multi_root_workspace.json before downgrading if preferences must survive. Disabling Skills does not remove previously delivered bodies from history or stop processes already started.

See ADR-0023 for the architecture and acceptance boundaries, and the progress ledger for implementation and platform evidence.

Panel

The Folders dialog at the sidebar foot lists the primary root and the additional roots, and offers add, remove, alias, read-only/read-write switching, copy path, and reveal in the file manager.

Model side

The additional-root topology is injected into the system prompt so the model knows which directories are available, and the information updates when grants change. No new tool needs to be exposed to the model.

Optional Auto Review integration

For multi-root reviewer context, enable the default-disabled Auto Review row in the main package’s Plugin Manager row list. The joint integration targets only DSH 0.2.1-rc.2; ordinary installation keeps the existing support matrix and does not enable or select Auto.

Project Structure

.
├── src/
│   ├── roots.ts                      pure rules: canonicalization, conflict validation, record classification (available/missing/redirected/invalid)
│   ├── registry.ts                   one storage core and Host facades, store-wide ordering of mutations and admissions
│   ├── registry-lease.ts             cross-process Registry Authority: POSIX flock (the kernel releases it on process death)
│   ├── registry-lease-win32.ts       ditto, the Windows named-semaphore implementation
│   ├── scope.ts                      ctx.multiRootScope: the single authorization source, plus the model-visible topology snapshot
│   ├── fs.ts                         the multi-root filesystem provider (in-process fence, extends the upstream LocalFileSystem)
│   ├── sandbox.ts                    the multi-root kernel-sandbox provider (extends the upstream LocalSandboxProvider)
│   ├── dialects.ts                   Seatbelt / bwrap / Landlock profile recognition and additional-grant assembly (unrecognized shapes fail loudly)
│   ├── windows-*.ts                  the Windows root-set ACL runner and restricted-token execution
│   ├── containment.ts                path containment (lexical fast path plus a dev/ino alias fallback)
│   ├── instructions.ts               discovery, incremental delivery, budget and revocation for an additional root's AGENTS.md / CLAUDE.md (top level plus the subdirectories this session touched, injected from agent/pre-step)
│   ├── lsp.ts                        a separate LSP workspace initialized per additional root
│   ├── workspace-files.ts            host-side routing and confinement for additional-root file browsing in the Files tab
│   ├── compat.ts                     the multi-root-compat startup gate (version allowlist and mixed-install detection)
│   ├── compat/                       the version-difference adapters: dsh-version / sandbox-confine / agent-instructions / llm-message, etc. (optional peers loaded on demand)
│   ├── command.ts                    the /workspace-folders command and the host half of the panel RPC
│   ├── contract.ts                   the panel wire protocol (zod-validated on both ends, inlinable into the browser bundle)
│   └── client/                       the browser half: sidebar action, the Folders dialog, the Files-tab root switcher, bilingual dictionaries
├── tests/                            differential parity, real-execution dialect matrix, contract round-trips, component and locale gates, cross-process lease e2e, instruction injection
├── scripts/                          smoke batteries (compose / behavior / journey) plus the docs, kernel-runner, compatibility-contract and upgrade checks
└── docs/                             requirements, architecture, decision records (ADRs), plans, development workflow

Contributing

Issues and PRs are welcome:

  1. Read AGENTS.md (repository-wide rules) and the development workflow (environment, commands, the runtime support matrix, the upgrade procedure) first.
  2. Branch from main; follow conventional commits (feat / fix / perf / refactor + scope) — see the existing history.
  3. All of these must pass locally before a PR: pnpm lint, pnpm typecheck, pnpm build, pnpm test, pnpm docs:check, pnpm smoke (CI runs the same order, build before test).
  4. Behavior changes update the matching document under docs/ in the same change; keep the README bilingual pair in sync; record engineering decisions that involve trade-offs as ADRs.
  5. Hard constraint: never modify any package of the upstream repository (deepseek-harness); the plugin stays an out-of-tree extension.

Known Limitations

  • An additional root's instruction files arrive as they become relevant: the one at the root's top level is injected before the session's first step, and a subdirectory's is injected once the session has successfully worked in that directory. A nested file is re-examined only when its directory is examined (the root's own every step; a subdirectory's because it was delivered from, or because of a fresh touch), delivery state is in-process (so a resumed session may be told again), and only the read / write / edit tool names count. The primary root's chain and the user-global file remain upstream's job, and this plugin does not inject them a second time (ADR-0010).
  • Hosts share one Registry service: one Host owns storage and each instance obtains admission for new operations. Disconnection stops new additional-root grants; a successor takes over through the kernel lease. Sharing is local to one user and lock namespace; Linux requires SO_PEERPIDFD, and Windows is limited to one login session. Existing processes and PTYs retain their launch snapshot (architecture, Chinese).
  • The supported matrix is an exact-version allowlist: on a host whose release is not on the list, or whose core packages disagree about the release, the fs / sandbox / registry / instructions / lsp / workspace-files rows do not start and the composition degrades to "the plugin is not installed" (ADR-0009).
  • The Windows multi-root runner inherits upstream partial enforcement, caller-owned directory requirements, and private-temp isolation. Running processes retain their initial roots; later processes use the updated set (ADR-0011).
  • Network and device paths cannot be additional roots on Windows: UNC (\\server\share), \\?\, \\.\, and mapped network drives are rejected at registration with unsupported-path — the restricted token's capability SID cannot be shown to bind SMB access (ADR-0011 amendment).
  • The confined Windows shell is pwsh; Git Bash is not available: MSYS cannot start under the write-restricted Low-integrity token (identically under the upstream and plugin runners — an upstream limitation). An upstream-confined child also cannot receive Ctrl+C (it inherits the runner's ignore flag); under the plugin runner Ctrl+C reaches the child and the runner mirrors its exit code.
  • Additional-root access is three-state: new registrations default to inherit (they follow the session mode), --read-only refuses writes in every mode (including Full-access), and a fixed --read-write stays writable even in a read-only session — it is a question of write authority, never read confinement (ADR-0017). A running process or persistent terminal keeps the deny set it was started with — a terminal started before a root turned read-only stays unrestricted until restarted; cap and access changes constrain only later calls and new processes (ADR-0013); hosts with workingDirectory may use an additional root as the execution directory for later tools, while the session origin and registry identity stay unchanged (ADR-0012).
  • On Linux hosts where upstream selected Landlock, a restricted Full-access needs bubblewrap: Landlock's allowlist cannot express "everything writable except these roots", so that combination selects bwrap to spell the deny boundary; on a host without a usable bwrap (not installed, or unprivileged user namespaces disabled), a Full-access command that carries any deny boundary (a read-only root, or the protected $DSH_HOME) fails closed at launch. Enable bwrap there, or avoid explicit read-only roots under Full-access on such hosts (ADR-0017).
  • Browse additional roots from the Files tab's root switcher: the header entry lists the primary root and every additional root, with the selected root's full path still shown beside it; selecting one lets a byte-for-byte fork of the upstream Files tree take over the tree area (lazy-loaded directories, native rows and icons, watch-driven auto-refresh, file clicks opening in the host viewer, plus a manual refresh in the header's reload spot), and the header's open-in-app button is simultaneously replaced by a byte-for-byte fork of the upstream control that opens the selected root instead of the primary one; switching back to the primary root restores the native view. The dialog is management-only again. The takeover relies on upstream data-* DOM hooks and degrades silently to the native look when they stop matching (ADR-0015, ADR-0016). LSP uses the containing additional root as its own workspace; the host must configure the relevant language server (ADR-0011).
  • The command's own output text is English (a host-side handler has no active locale to consult); the panel is bilingual.
  • The Workspace Folders panel is a sidebar footer action plus a dialog rather than a full panel: sidebar.footer.action is offered by every supported release, whereas sidebar.panellist / main are not.

Documentation

Long-term project documentation lives in docs/:

Repository-wide rules for Coding Agents are defined in AGENTS.md.

License

MIT