dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
A DeepSeek Harness Plugin that add a new permission that every tool call opens an approval card first
A fourth permission mode for DeepSeek Harness: Manual. Every tool call —
including a plain file read, a grep, a glob, or a web search — opens an
approval card first. You see the tool name, every argument on its own line, and
an optional feedback box; your decision is the only thing that lets the call run.
The three shipped modes each make one trade-off for you:
| Mode | Sandbox | Approval |
|---|---|---|
| Read Only | read-only | on demand |
| Workspace Write | workspace-write | on demand |
| Full access | danger-full-access | never |
"On demand" means a tool decides when to ask, and a tool that does not ask runs immediately. "Never" means nothing asks at all. Manual closes that gap: not one tool decides — you decide, every single time, with the arguments in front of you.
ask for all tool calls in a
Manual session, before any other pre-execute listener can answer.danger-full-access, so an approved call has
full execution authority, including writes outside the workspace. There is no
platform-specific code in this plugin — it works the same on Linux, macOS and
Windows.description
of what the command does.Installing adds the package, links the plugin into the profile, and applies the
bundle's patch. The new preset and the Host gate take effect on the next
profile start: restart dsh web (or your profile) after installing.
dsh plugin --profile web add dsh-manual-approval
Or from a checkout:
git clone <this repo> dsh-manual-approval
dsh plugin --profile web add ./dsh-manual-approval
dsh --profile web --dump-config # verify composition
Then pick Manual in the permission selector, or run /permission manual.
To remove it:
dsh plugin --profile web remove dsh-manual-approval
npm test # the offline suites
npm run test:boot # boots a real `dsh web` on a throwaway profile
dsh --profile web --dump-config is the composition check; look for the
manual-approval row and for id: permission marked as patched by
dsh-manual-approval.
The package ships its own cordis.patch.yml (dsh.bundle.patch), so installing
it is enough — you never edit the host composition by hand. The patch does two
things:
permission preset table with manual added first, so
the selector shows Manual at the top;Compatibility note. Entry 1 restates the built-in presets so the table is complete. If a future Harness release adds a fifth built-in preset, this file must be updated to include it. Removing the plugin removes the patch with it.
While Manual is the active mode, each call is first checked against your own ordered rule list (Settings → Manual mode rules). The first matching rule decides; with no match, the call opens the normal approval prompt.
A rule is a list of conditions (all must match) plus one action.
Each condition picks one target and one regular expression:
| Target | The regex is tested against |
|---|---|
| Tool name | the whole tool name, e.g. ^pwsh$ |
| Parameter name | each argument name in turn, e.g. ^(command|cmd)$ |
| Parameter value | each argument value in turn, e.g. rm\s+-rf |
Conditions never need to escape JSON: file_path matches a parameter name,
C:\Users matches a path value.
| Action | Effect |
|---|---|
| Allow | the call runs immediately, no prompt |
| Deny | the call does not run; its reason is returned to the model |
| Warn at approval | still asks you, but shows your text in red in the card |
| Network judge | POSTs the call to your endpoint and applies its verdict |
A catch-all rule — one condition, target Tool name, pattern .*, action
Allow, placed first — makes Manual behave exactly like Full access. That is the
escape hatch, and it is why rules are reorderable.
A rule's reason (or warning) may reference capture groups as $1, $2, … or
${12}; $$ is a literal $. Groups are numbered globally across the rule's
conditions, in condition order and left to right, and the settings page shows
each condition's group numbers next to its pattern so you never have to count
them.
Values are taken from one combined JSON document built from the whole call, because a captured parameter may come from any part of it:
{"<toolName>":{"<argName>":"<argValue>", ...}}
So a pattern like "command":"([^"]+)" can capture the command text even though
its own condition matched a parameter name. A group whose pattern does not match
that document contributes an empty string.
This is the matching document only. The network judge receives a different, richer document (see Network judge), so what an endpoint sees never changes which rules match.
An automatic denial goes through the same formatter as a denial you type in the approval card, so the model cannot tell a rule's rejection from a hand-written one. There is no "automatic denial" marker anywhere in the tool result.
The approval panel and the settings page ship English and Simplified Chinese
copy, registered through the DSH locale service (ctx.locale), so they follow the
language chosen in Settings → General → Language — switching it re-labels the
page and the card without a reload. English is the fallback for any key the active
language misses, and it also answers when no locale service is composed at all, so
the UI is never keyless.
Rule patterns and the technical field names stay as they are: a regular expression and a JSON path are not prose.
The judge runs in the Host process, not the browser. That matters: Node has
no same-origin policy, so http://127.0.0.1:…, a plain-http intranet service,
or an endpoint without permissive CORS headers all work. A browser fetch would
fail on CORS and mixed content, and routing it through the page would turn the
Harness into an open proxy.
POST with content-type: application/json by default; method, headers and
body are all editable.
The default body describes the session and the call:
{
"session_id": "…",
"working_space": "D:\\projects\\app",
"cwd": "D:\\projects\\app\\src",
"tool": "pwsh",
"args": [{ "name": "command", "value": "ls" }]
}
Both directories are sent, and they can differ. working_space is the project
you chose for the session: it never moves, so it answers "did this call leave
the scope I picked?". cwd is where the call actually runs right now — the
agent can move it with the working_directory tool — so it answers "what does
this relative path mean?". A judge needs both; with only the workspace,
relative paths resolve against the wrong folder, and with only the current
directory there is no way to notice that the call left your project. Unknown
facts are omitted rather than sent as empty strings.
An override body may use $1, $2, … / ${N} for capture groups and
${session_id}, ${working_space}, ${cwd}, ${tool} for the session facts.
Every substituted value is JSON-escaped, so keep placeholders inside quotes; an
unknown ${name} is left as written so a typo stays visible.
decisionPath (required) and reasonPath (optional) are dot/bracket paths
into the response — result.decision, choices[0].verdict, ….
The decision value must be "approve", "deny" or "ask", corresponding to
Allow, Deny and Warn at approval.
The reason from reasonPath is a literal string: it is not run through
capture substitution.
Any failure — unreachable host, non-2xx, invalid JSON, missing path, unexpected value, timeout, over-large body — falls back to a red warning showing the concrete error, and the call still comes to you. A judge can only ever ask; it never fails open.
Credentials written into the URL (http://user:pass@host/) are stripped from
every message, warning and log line. Note that Node's fetch refuses such
URLs outright, so send a token as a header instead.
Rules live at <DSH_HOME>/manual-approval/rules.json, written atomically, and
are re-compiled on every read. A rule with a syntax error is kept on disk and
reported in the settings page rather than being silently dropped, and it cannot
be enabled until it is valid. A master switch disables the whole list at once.
See examples/ for a ready-to-edit document, including a warning
pattern for destructive recursive deletes.
A second setting, off by default: Reject with the rule warning.
When a rule only warns — a warn rule, or a network judge that returned
ask together with its reason — the approval card shows that text in a red
exclamation mark, and the call still comes to you. If you then press reject
without typing a reason of your own, the plugin sends that warning text to
the model as your rejection reason instead of "left no rejection reason".
This is the main reason the setting exists: the judge server usually explains why it wants a human, and without this option that explanation dies with the card while the model is told only that you refused.
It is deliberately narrow:
The sentence still comes from the same formatter as a typed reason, so the model cannot tell the two apart.
The feedback box and the settings page need a Client → Host path, and the shipped approval outcome vocabulary is four fixed strings with nowhere to carry text. This plugin adds one private loopback HTTP route instead.
What that route exposes, and what it does not:
0.0.0.0; without it, another machine on the
network could forge approval feedback or rewrite your rules.<DSH_HOME>/manual-approval/, never touches
credentials, and never returns Host state.If your composition has no webServer, the plugin still gates every call and
rules still work; only feedback delivery and the settings page are disabled.
sandbox_permissions/justification meaningless. Set strictGuard: 'bypass-guard' in the plugin config to deny any Manual-mode call that still
carries them.ctx.tools and so is never gated here.All optional, in the host row config:
- id: manual-approval
name: 'dsh-manual-approval'
config:
presetName: manual # the preset key that activates this policy
feedbackPath: /dsh-manual-approval/feedback # loopback route for feedback + rules
rulesFile: '' # override the rules.json path
strictGuard: 'off' # or 'bypass-guard'
debug: false # log each decision
See DESIGN.md for the exact seams, the decision order, and the rule engine's semantics.
MIT