dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Load each workspace's direnv environment into every DeepSeek Harness command. 让 DSH 每个工作区的子进程自动带上该工作区的 direnv 环境。
dsh plugin add dsh-direnv
Every command the harness runs then inherits the direnv
environment of its own working directory. Two minutes to install; needs
direnv on PATH.
~/.dsh $ dsh web # no .envrc here
└── session in ~/projects/api
└── bash: build → has `cargo`, `DATABASE_URL`, …
direnv version. If it fails, install direnv first — this plugin never runs direnv allow for you.# once it is on npm
dsh plugin add dsh-direnv
# from a checkout instead: `npm install` runs the build that produces lib/
npm install && dsh plugin add "file:$PWD"
disabled: true:dsh --profile <name> --dump-config | grep -A3 'id: subprocess'
.envrc.bash -c 'echo $SOME_VAR_YOUR_ENVRC_SETS'.PATH diff in priority order.An empty line from step 2 means the environment is not loading. Go to Troubleshooting.
The plugin takes over the harness's subprocess seam and evaluates
direnv export json per working directory. Three flows share that one fact
source and never block each other:
| Flow | Behaviour |
|---|---|
| Injection | spawn looks the directory up in a cache and merges the diff into the child's environment. The lookup is synchronous and never waits: an unknown directory is forwarded untouched while its evaluation runs in the background, and spawnTerminal (a PTY you opened) awaits the evaluation first. |
| Gate | While the session's environment is unknown, tool calls are held — the model may talk and may ask for tools, but those calls wait as though the tool itself were slow. The gate arms once per session, waits at most loadTimeoutMs, and is a delay rather than a filter: a determinate result, the timeout, and a cancellation all release it. A released call does not cancel the evaluation; that keeps running under its own budget (evaluateTimeoutMs, 0 = never kill). |
| Surfaces | One sourced notice tells the model what was loaded. The right-sidebar tab tells you the variable names, the PATH diff (added, removed, unchanged) and the credential list, drawn entirely from DSH's own UI components so it follows your theme. |
Direnv does the walking: the anchor is each spawn's own resolved cwd, so a
command that cds into a subdirectory gets that subdirectory's environment, and
a change to any .envrc takes effect on the next tool call.
- id: direnv-subprocess
name: 'dsh-direnv'
config:
enabled: true
direnvPath: '' # empty = resolve `direnv` from PATH
loadTimeoutMs: 300000 # tool calls wait at most this long, then proceed
evaluateTimeoutMs: 0 # kill the direnv child after this long (0 = never kill)
gateTools: all # all | spawning | none ('spawning' currently behaves like 'all')
injectSensitive: all # all | filter
notifyModel: true # one sourced message per workspace state change
sidebar: true # right-sidebar tab and composer placeholder
disabledDirs: [] # absolute paths to leave alone
injectSensitive: filter withholds variables whose names look like credentials (/KEY|PASSWORD|SECRET|TOKEN/i). Cautious setups only; it breaks dev shells that need those values.loadTimeoutMs is the gate's budget: how long a tool call may be held before it runs with whatever is known.evaluateTimeoutMs bounds the direnv child. 0 never kills it, because a cold use flake has no predictable duration. The price: an .envrc that never finishes keeps one direnv process per directory alive for the session.Almost every failure here is quiet. Read startup output before assuming it works. These five cover nearly all of it:
| What you see | What it means | What to do |
|---|---|---|
dsh: skipping profile bundle "dsh-direnv" | Peer range mismatch, so the whole bundle was skipped. Its patch never ran and --dump-config looks identical to a clean install. | Install a DSH the range accepts, or accept the risk with dsh plugin allow-version. |
dsh: disabling profile plugin row "direnv-subprocess" | The row-level preflight disabled it. --dump-config does not show this. | Same as above. |
dsh: warning: N entries did not activate + … pending (waiting for service: subprocess) | The patch applied but the plugin never came up, so commands have no bash tool at all. | Look for direnv-subprocess (…): failed to import; check the package is installed and its exports resolve. |
Error: service "subprocess" has been registered at <LocalSubprocessRuntime> | Both patch rows were not applied together, or the patch missed its target. | Keep both rows, keep the bundle after @deepseek-ai/dsh-base. |
| The web app exits non-zero without printing a URL, right after a checkout install | exports point at lib/, which only exists after a build. | Run npm install (its prepare builds) or npm run build. |
| What you see | What it means | What to do |
|---|---|---|
patch: entry "subprocess" not found (only in --dump-config stderr) | The bundle was composed before the row it patches. | Move it after @deepseek-ai/dsh-base in dsh.profile.bundles. |
| The right-sidebar tab never appears | The client half was dropped silently. | dsh.client.platform must be exactly "web", and every dsh.client.inject entry must be an exact package name — a <pkg>/client suffix is not normalized and matches nothing. |
--dump-config does not predict everything: it composes patches without the
row-level preflight. Patch warnings appear only there; compatibility refusals
only at startup. Read both.
If the plugin is up but a workspace has no environment, open the sidebar tab: it
reports blocked, unreadable and absent separately, and says when a
directory is merely indistinguishable from one without an .envrc.
.envrc is arbitrary code. Evaluating one runs bash as you, outside any sandbox. The plugin only evaluates files direnv would already evaluate, and never approves one for you.injectSensitive exists.DIRENV_* from the environment it hands to direnv, so a harness started from a direnv-loaded shell cannot resurrect variables the harness scrubbed./plugins/* carrier.shell, subprocess, search, git and PTY consumers..envrc (chmod 000) or a direnv denyed directory produces the same empty output as "no .envrc here"..envrc can half-succeed. direnv exits 0 for source misses and syntax errors with earlier variables already applied. Both the sidebar and the notice report these as degraded.KEY, PASSWORD, SECRET or TOKEN (including TURKEY_MODE) are scrubbed before an overlay applies. Only values a .envrc sets explicitly survive.direnv keeps no cross-process cache, so a use nix workspace costs roughly 0.2 s of re-evaluation per tool call (seconds when cold). Cheap directories are validated by re-fingerprinting instead: content hashes for .envrc, .env, direnv.toml, lib/*.sh and the .direnv/ files nix-direnv rewrites; mtime+size for ordinary watch_file targets..envrc is not killed by default. Set a finite evaluateTimeoutMs if you would rather bound it.direnv, the bash that sourced the .envrc, its descendants — but the .envrc may already have written files or started a service. Nothing here prevents an .envrc from running; that is what the allow library and enabled: false are for.npm run typecheck # three projects: host, browser half, tests and harness
npm test # unit tests: evaluator (real direnv) + host wiring
npm run test:client # client half: boot row, byte-identical module, and the fenced status route
test/harness/run.sh --all # end-to-end cases, no API key needed
Strict TypeScript, no build on your side for the published package: src/**/*.ts
compiles per file to lib/ (host half), and client/**/*.ts is bundled into the
single file lib/client.js the browser is served. npm install builds through
prepare; lib/ is not committed. DESIGN.md records the architecture and the
measured evidence behind every decision.
npm test uses an explicit test/*.test.ts glob on purpose: bare node --test
would also execute the scripts under test/harness/. Invoke the harness through
run.sh, not node test/harness/run.ts — the wrapper scrubs
DSH_HOME/DSH_PROFILE_DIR, and the harness refuses to run against a live DSH
home by design. The harness drives a stub LLM against dsh headless, so a real
turn with real tool execution runs offline.
# build the checkout once (`npm install` runs the build through `prepare`)
cd /path/to/dsh-direnv && npm install
# a demo workspace in a scratch home
export DSH_HOME=$(mktemp -d)/dsh-home && mkdir -p "$DSH_HOME" /tmp/demo/proj
cd /tmp/demo/proj && printf 'export HANDS_ON=yes-from-envrc\n' > .envrc && direnv allow
dsh --profile demo --from-default-profile web --dump-config # seed a profile
dsh plugin --profile demo add "file:/path/to/dsh-direnv" # appends our bundle last
dsh --profile demo --no-open --port 30100 # open the printed URL
Open that URL, start a session in /tmp/demo/proj, and look for the direnv tab.
The host half can also be checked from the outside — the cookie comes from the
printed ?token= link:
curl -s -H "Cookie: $COOKIE" \
"http://127.0.0.1:30100/plugins/dsh-direnv/status.json?dir=/tmp/demo/proj&force=1"
# {"ok":true,…,"status":{"state":"ok","envrcPath":"/tmp/demo/proj/.envrc","variables":[{"name":"HANDS_ON",…}]}}
A scratch home has no model provider configured, so the sidebar renders but a real conversation will not. To see the model-side behaviour, point your own configured home at the plugin for one run — an absolute-path entry mounts the host half only (it is not a package, so it has no manifest and no sidebar tab):
cat > /tmp/dsh-direnv.yml <<'EOF'
- id: subprocess
disabled: true
- insert:
- id: direnv-subprocess
name: '/path/to/dsh-direnv/lib/index.js'
EOF
dsh --profile web --patch /tmp/dsh-direnv.yml --no-open --port 30100
# then, in a workspace with an .envrc, ask the model to run:
# bash -c 'echo $YOUR_VAR'
Never run
dsh plugin addagainst a profile your Nix configuration manages: it replaces the profile's symlinks with real files. An absolute-pathinsert(above) or the Nix-side route inDESIGN.md§6.3 are the non-destructive options.
MIT