dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Stops a DeepSeek Harness agent from reading your credential files and pasting secrets into tool calls
Data-loss prevention for DeepSeek Harness, built as an out-of-repo plugin.
ctx.tools.guard(), testing path-typed arguments against a table of credential stores and
following symlinks first.AGENTS.md/CLAUDE.md chain, a captured tmux pane, a hook's
additionalContext, a skill body a /name token loaded) and the input the loop claimed from
the inbox that the user did not type (a dsh-webhook delivery's third-party payload, a
settled subagent result, an agent relay). All of it reaches the model and the durable log
through agent/pre-step without ever being a tool result. A message whose source.kind is
user is exempt below aggressiveness: high; at high the user's own typed prompt is
redacted too, because this plugin cannot know which provider the request is bound for. The
agent/inbox/spliced delivery record keeps a delivery's original text, which is deliberate —
it derives no model message, and an operator investigating a webhook incident needs to read
what was actually delivered.DSH_TELEMETRY_MODE=FULL
ships message text, tool arguments, results and workspace paths in the clear.aggressiveness: high) a prompt someone typed.dsh-auth- browser cookie
and the 43-character launch token that mints it. Whoever holds either is an authenticated
caller of the Host API that drives the agent. DeepSeek Harness Desktop is an Electron shell
around the Web application, so under Desktop that credential always exists; the CLI
composition this plugin was first built against runs no web server and has neither.CLAUDE.md, .claude/rules/** and the other agent rules directories, prompt
templates, .vscode/tasks.json, .mcp.json, git hooks, CI workflows, shell startup files,
pnpm-workspace.yaml — and before it writes a *_BASE_URL that would redirect a provider
credential.non_interactive: true,
approval_mode: auto, an apply whose approval is still pending. Both ask tiers are
prompts rather than controls: they live at tools/pre-execute, they can be neutralised, and
they abstain wherever the approval seam prompts nobody — which includes every install under
DSH_PERMISSION_MODE=danger-full-access and a stock headless install under any mode.dsh-dlp report reads it back.This is not a containment boundary. The plugin runs in-process, at the agent's own uid.
Anything the agent can execute — a bash command, a run_code program, a mounted MCP server —
can read every file the guard denies and open its own sockets without the plugin seeing anything.
It closes the path where the model asks for credential material through a tool. It does not stop
code that is already running. If you need containment, that is the sandbox, landlock-run,
filesystem permissions and egress firewalling.
Three limits worth knowing before you rely on it:
ctx.tools.guard() is order-independent only because it has no allow
arm. Result redaction registers with { prepend: true } so it gets the last word over
listeners already registered — but a listener registering after it with the same option lands
ahead of it again.python3 -c "open('~/.ssh/id_rsa')" is denied. Changing the spelling
defeats it — one glob character, quote-splitting, find -exec, a substitution that assembles
the path from pieces, a base64 round-trip, each verified. Do not count this arm as a
control.Desktop is an Electron shell around the Web application, not around the CLI. Its profile mounts the Web bundle plus a bundled Python/LibreOffice runtime and the three Office skills, so three things change for this plugin and only one of them is closed here.
python-docx/openpyxl/python-pptx arrives as a tool result and is redacted there. So does
the structural checker's JSON, load_workspace_dependencies' path report, and a past session's
transcript that session-reference splices into a step — that last one only because this
plugin registers its agent/pre-step listener after the bundle does, which is the ordering
caveat above and not a guarantee.read_image.
An image content block carries an attachment reference and no text, so there is nothing for a
text detector to scan: a card number or a credential printed inside a .docx reaches the model
as pixels. Neither is Electron's userData directory on the credential-path table — it holds
the Host cookie jar — because the Host child process is never told where it is.A profile carrying only @deepseek-ai/dsh-base has no agent loop, so add a runnable bundle
alongside it or there is nothing for this plugin to guard:
dsh plugin --profile <name> add @deepseek-ai/dsh-headless@0.1.0-rc.6
dsh plugin --profile <name> add dsh-dlp
dsh --profile <name> --dump-config # the dsh-dlp row should appear
Any harness from 0.1.0-rc.6 onwards in the 0.1.x line works, prereleases included. CI drives
the end-to-end suite against every published rc the peer ranges admit — 0.1.0-rc.6, rc.7,
rc.8, 0.1.1-rc.1, 0.1.1-rc.2. The 0.1.2 prerelease line is watched by a non-blocking job
line without running it end to end.
Pin @deepseek-ai/dsh-headless explicitly — its npm latest tag still points at 0.0.1-rc.1.
The package ships a cordis.patch.yml bundle layer, so listing it in dsh.profile.bundles mounts
it with working defaults.
Install from the registry or a packed tarball, not from a git spec: lib/ is a build output
git does not carry and no prepare script rebuilds it, so a git-spec row mounts and then fails to
load.
- id: dsh-dlp
config:
auditLog: /var/log/dsh-dlp.audit.jsonl
redactionKeyFile: /var/lib/dsh/dsh-dlp.redaction-key
policyFile: ./.dsh-dlp.yml # optional, lowest trust
aggressiveness: medium # low | medium | high
breadthTier: true
resultRedaction: true
telemetryRedaction: true
stepContextRedaction: true
claimedInputRedaction: true
configWriteAsk: true
approvalSuppressionAsk: true
redactionKeyFile is created on first mount with 32 random bytes at mode 0600. Keep it out of
version control — it is what makes a placeholder's hash keyed rather than a bare digest anyone
holding a candidate secret could confirm.
aggressiveness is one word for how far redaction reaches. low guarantees nothing and lets
each toggle stand alone; medium — the default — guarantees every pass is on and that no toggle
can take one away; high adds the user's own typed prompt to what is redacted. It composes with
the toggles rather than overriding them: at medium and high a toggle set to false
contradicts the level and fails the mount with the fix in the message, rather than one setting
quietly beating the other.
Upgrading from 0.9.0. The default
mediummatches the shipped toggle defaults exactly, so an install that never wrote a toggle is unchanged. An install that set any redaction toggle tofalsenow refuses to mount; addaggressiveness: lowto the same row and it means what it meant before.highis opt-in.
The guard floor has no configuration. Credential-path denial and secret-argument denial are
security invariants, not deployment-varying tunables. A repo-local policyFile is the lowest
trust rank and may only tighten: add deny patterns, add egress tool names, raise a severity,
switch a pass on. It cannot reach aggressiveness — raising the level would let a hostile
workspace put placeholders into the user's own prompt. Any downgrade makes the whole file
invalid.
Configuration reference → · What gets denied → · Redaction and detection →
dsh-dlp report # everything in the audit sink
dsh-dlp report --since 24h
dsh-dlp report --session <id>
dsh-dlp report --would-have # everything except the denials
A redaction or denial record carries a rule id, rule version, span offsets and a keyed hash — never the matched value. An ask carries its rule id, a rewritten call the names of the fields that changed, and a neutralised remote image the destination hostname in the clear; none of those has a matched region to hash.
Three registrations work around defects in DeepSeek Harness rather than in your configuration:
remote markdown images in assistant output, a tool call rewritten between tools/pre-execute and
the guard, and a telemetry redactor that cannot run under the shipped default. None of them
closes its channel and an upstream fix is better in all three cases.
What each one does and does not close →
nvm use 22 # Node ^22.19.0 || >=24, and pnpm 11
pnpm install
pnpm run typecheck
pnpm run test:coverage
pnpm run test:e2e # boots a real dsh against a mock model; no API key
test:e2e needs a harness to boot. It defaults to a checkout at ../dsh, which must have its
dependencies installed, build:lib:host run, and native/system built; DSH_CLI points it at an
installed dsh bin instead. It cannot currently run against the 0.1.7 line even though the
peer range admits it: the pinned mock server speaks /chat/completions and that line's adapter
speaks /v1/messages. ADR §34 records the options and why none was chosen unilaterally.
Coverage is gated at 100% per file: this is a security control, so an untested branch in a deny path is an unproven deny path.
Design decisions and their rationale live in ADR.md. Security policy is in SECURITY.md.
MIT