dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
DSH plugin for per-session working directories: keep parallel agent sessions from overwriting each other.
Per-session working directories for DSH — the Codex-style behavior, as an opt-in per project, switched from a page in the Web GUI, from a conversation, or from a marker file.
English | 中文
DSH gives every session the Workspace directory you picked, so two sessions in one project write into the same files. Run three agents on one folder and they overwrite each other's output: the second one "fixes" the file the first one just wrote.
While isolation is on for a directory, every new session created there works
in its own session-<id> subfolder. That subfolder — not the project — becomes
the session's workspace-write sandbox root, so parallel sessions cannot touch
each other's files, while the project itself stays readable to all of them.
project/ ← the Workspace you picked (readable to every session)
├── .dsh-isolate ← the switch (an empty file; delete it to turn isolation off)
├── src/ … ← the project's own files (read-only to sessions)
├── session-1a2b3c4d/ ← session A's sandbox root, private to it
└── session-9f8e7d6c/ ← session B's sandbox root, private to it
-2, -3, … if two ever collide).AGENTS.md discovery and project-skill lookup still work — both walk up from
the session's cwd.Nothing in DSH is modified. The plugin mounts through the ordinary Cordis composition, wraps two seams at load time, and removes both when it unloads.
No build step and no runtime dependencies beyond DSH itself. The repository
contains the Cordis entry, six lib/ modules, the browser half, the bundle
patch, and the test suite. Verified against DSH 0.1.6-alpha.2; Node.js 22+ is
required.
dsh plugin --profile <profile> add \
https://github.com/sweet-boby/dsh-session-isolation.git
Then restart the host once. The plugin manager installs the package, selects its
bundle, and applies cordis.patch.yml; the restart lets the
browser module table discover the new dsh.client bundle.
Open Plugins → Add plugin and paste the GitHub URL:
https://github.com/sweet-boby/dsh-session-isolation
If that DSH build's installer only accepts a local bundle path, clone the repository first and paste the checkout's absolute path instead.
git clone https://github.com/sweet-boby/dsh-session-isolation.git
dsh plugin --profile <profile> add \
/absolute/path/to/dsh-session-isolation
Add the dependency and select the bundle in
~/.dsh/profiles/<profile>/package.json:
{
"dependencies": {
"dsh-session-isolation": "github:sweet-boby/dsh-session-isolation"
},
"dsh": {
"profile": {
"bundles": ["…", "dsh-session-isolation"]
}
}
}
For local development, replace the dependency with
file:/absolute/path/to/dsh-session-isolation, then install in the profile:
cd ~/.dsh/profiles/<profile>
pnpm install --ignore-scripts
Restart the host after either installation path.
sessionController, workspaceRegistry, and tools services
composed — every base-backed profile has them. A deployment missing one leaves
the plugin pending instead of half-applied.ctx.webServer) is optional: without it the plugin keeps the
policy and the tool and simply serves no GUI page.Isolation is a property of the directory, and its durable form is a
.dsh-isolate marker file inside it, so the setting travels with the folder and
a file manager can change it too.
设置 → 内置插件 → 工作区隔离). One row per Workspace with its path, session
count, and a single switch. The list also refreshes every five seconds, so a
decision made anywhere else shows up here.session_isolation tool: status, list, enable, disable, with an
optional absolute path that defaults to the calling session's workspace.await ctx.isolatedSessions.setIsolated(workspaceId, true)
or ctx.isolatedSessions.setIsolatedPath(path, true); list() and
isIsolated(id) read the current state..dsh-isolate file in the project directory.Start a new session in that Workspace and ask it for its working directory
(pwd, or just have it write a file). The answer is a session-<id> subfolder,
and the project root appears in its file policy as a read-only location. Two such
sessions get two different folders.
The browser half is a hand-written client bundle (lib/client.js)
declared with dsh.client and exports["./client"], so the Web client module
table serves it like any packaged plugin bundle. It registers one
settings.plugins.tab entry through ctx.slots.inject, i.e. it waits for the
Settings section to declare that slot before registering — plugin activation
order is not stable, and registering into an undeclared slot fails the whole
browser boot.
An external plugin's browser half cannot declare a Remote namespace, so the two halves talk over two named routes on the Web carrier:
| Route | Method | Body | Answer |
|---|---|---|---|
/session-isolation/workspaces | GET | — | { workspaces: [{ workspaceId, title, path, isolates, sessionCount }], isolatedCount } |
/session-isolation/toggle | POST | { workspaceId, enabled } | the refreshed list, or { error } |
They hold no state of their own — every answer is projected from the marker files — and they are unauthenticated routes on the loopback-bound carrier, like the other plugin asset routes. A host without a Web carrier registers neither.
One restart is needed for the page to appear. The client module table scans
dsh.clientdeclarations at activation and caches its verdict per package, so a bundle that appeared after the host started is picked up on the next boot. Host-side edits reach a running host through configuration reload; a new client bundle does not. Until then, every other switch (tool, marker file, host service) already works.
Two seams, wrapped at load time and unwrapped on unload. Both delegate to the
original for anything they do not own, and both fail loud — … this DSH build moved it — rather than silently isolating nothing.
ctx.sessionController.create — the Remote entry point the GUI calls.
DSH resolves a new session's cwd as workspace.path ?? request.cwd ?? default,
and the controller rejects a request carrying both workspaceId and cwd, so
the plugin points the named Workspace entity at a freshly allocated private
directory for the duration of that one call. The durable record is untouched,
the redirect is removed in a finally, and a failed creation deletes the
folder it allocated. A mutation slot absorbs the entity's own writes during
the redirect, so a session accounted in that call is never rolled back when
the redirect is removed.WorkspaceEntity reads (the registry builds one and shares it). A session
directory inside an isolating Workspace resolves to that Workspace's root, so
the stock attach validation, the durable membership prune, the one-time
history bootstrap, and the "one owning Workspace per session" invariant all
keep working with no source change.The isolation state itself is never persisted by the plugin: it is recomputed from the marker files, so a restart, an external edit, or another tool all converge on the same answer. Scans are serialized, and a scan that lands while a per-call redirect is in flight merges instead of retracting a decision it cannot currently see.
git works against the parent repository but a new branch is
still shared. Branch-per-agent isolation needs a worktree, which this plugin
does not create. For a normal coding repository, leave isolation off: the
agent could not edit the repository's own files.workspace-write mode; a session can still read the project. A user can also
approve danger-full-access for a turn, which bypasses the boundary — that is
DSH's approval policy, not this plugin's.file: installs are copies. pnpm copies a file: dependency into the
profile rather than linking it, so after editing the plugin re-run
pnpm install (or delete node_modules/dsh-session-isolation and reinstall)
before the next host start.| Symptom | Cause | Fix |
|---|---|---|
| The settings page has no Workspace isolation tab | The host started before the client bundle existed | Restart the host once (see the restart note) |
/session-isolation/workspaces answers 404 | The plugin is not loaded, or the host has no Web carrier | Check the plugin is a selected bundle and installed; check the host boot for a failed session-isolation fiber |
| New sessions still share the project directory | The marker is not in the Workspace's own directory, or the session predates it | Confirm .dsh-isolate sits in the Workspace path shown in the page; only sessions created after it are isolated |
| Sessions from an isolating project appear ungrouped | The plugin is not active for that host | Re-enable the plugin; the folders and logs are intact |
| The agent cannot write a file in the project root | Working as designed | Turn isolation off for that project, or have the session write inside its folder |
.dsh-isolate), or leave the markers — they are inert without the plugin.dsh-session-isolation from the profile's dsh.profile.bundles and
dependencies, then run pnpm install --ignore-scripts in the profile.Session folders and logs are never touched by an uninstall.
node --test "tests/*.test.js"
| Test file | Covers |
|---|---|
tests/isolation.test.js | the marker policy, directory allocation, both seam wrappers, the state cache, the service API, and the HTTP routes against fakes |
tests/integration.test.js | the real dsh-storage → dsh-storage-domain → dsh-workspace stack plus a real dsh-tools runtime: session creation, concurrent sessions, membership, a restart over the same medium, and toggling |
tests/tool.test.js | the session_isolation tool's exported JSON Schema |
tests/client.test.js | the browser half's module-table handoff and its slot declaration wait |
tests/integration.test.js imports the built lib/ trees from a DSH checkout by
absolute path (the CHECKOUT constant at the top); adjust it when the checkout
moves.
index.js Cordis plugin: seam patches, service, tool, optional Web routes
lib/isolate.js the marker policy and session-directory allocation
lib/state.js the isolation cache: marker scans, redirect guard, serialization
lib/patch.js the two seam wrappers + the ctx.isolatedSessions service
lib/tool.js the session_isolation model tool
lib/web.js the two HTTP routes behind the GUI page
lib/client.js the browser half (Settings → Built-in plugins page)
cordis.patch.yml the bundle patch that inserts the plugin row