dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
新增「替我审批」访问模式:审批请求由独立的复核模型裁决而非人工,复核者可只读工作区,复核跑不起来时把请求交回人工,每次裁决的理由记录在「审批」页签中。
0.6.2 fixed only the removal direction, and the live host immediately showed the
other half: an enable is an HMR transaction as well, so the mount-time write
that restores a missing preset was rejected with HMR transactions cannot be nested (log: access-mode preset check failed — could not write preset "approve-for-me"). Both directions now share one transaction-free writer: leave
the store through hmr.executing.exit(...), then call configEditor.edit(),
which QUEUES behind the running reload instead of failing. The first attempt is
no longer delayed (the menu should not wait for it) and the retries cover a row
that is replaced mid-reload. Two regression tests join it: a write issued while
the mount runs inside the store must succeed, and an unleaveable context must
report the failure.
Diagnosing 0.6.1/0.6.2 surfaced another trap: inside one host process a reinstalled plugin reuses the module it first imported — a different install source or disk path changes nothing, and the mount log looked identical. A "fixed" version could therefore never have been loaded while the retest said otherwise. This release makes the running build visible (the ready line carries v0.6.3) and has the uninstall cleanup state whether the HMR transaction context could be left before it gives up. Restart DSH Desktop after upgrading the plugin; without a restart the new code is not loaded.
0.6.1 only deferred the write to a timer, and the live retest still failed: the host marks "inside a transaction" with an AsyncLocalStorage store, and Node propagates that store into timers created inside the transaction — so the write stays nested however long it waits, and all three retries hit the same lock. 0.6.2 leaves the context through hmr.executing.exit(...) before scheduling anything (falling back to the old behaviour when that service is absent), after which configEditor.edit() queues on the host's own serialized path. A regression test reproduces the mechanism with a real AsyncLocalStorage: the unmount happens inside store.run(true, …) and the write still has to succeed.
The first real uninstall of 0.6.0 exposed an ordering bug: the uninstall runs inside the host's HMR transaction, and writing the preset back into the profile patch takes that same lock, so the deletion failed with HMR transactions cannot be nested and the menu kept "Approve for me". The removal now runs outside the transaction and retries (0.25s / 1s / 3s); only three blocked attempts leave the entry behind, with an explicit warning in the log and in /approval-review status.
The "Approve for me" entry lives in the permission row's preset table, and an id-targeted override replaces that whole row — so any later layer that restates the table (typically a profile patch pinning its own defaultPreset) drops it without a word: the menu lists three modes, the plugin's gate key is not in the catalog, and it claims nothing. This release adds two guards: a mount-time catalog check that names the cause in the startup log and in /approval-review status, and (by default) a managed entry written into the profile patch through configEditor — present while installed, removed on uninstall, no stale option. For a read-only deployment, manageAccessModePreset: false warns without ever writing. See the access-mode entry's lifecycle.
Codex-style agent auto-approval for DeepSeek Harness. When an action crosses a boundary that the sandbox does not cover on its own, a second, independent reviewer model reads the proposed action and returns a verdict — so a human handles fewer routine prompts. Model judgements can still be wrong. Each reviewed decision leaves a full rationale in a dedicated Approvals tab.
This plugin implements the shape of Codex's Auto-review: an interactive approval request is routed to a reviewer agent instead of a person, the reviewer answers with a structured verdict, a denial is handed back to the calling model as reasoning rather than as a bare error, and a per-turn rejection circuit breaker stops the agent from looping on escalation attempts.
It is a reviewer swap, not a permission grant. The plugin never widens a sandbox, never invents a grant, and never removes a human from a decision it was not configured to take over. Requests it does not own are delegated with
next(), unchanged.
reviewer.engine now chooses who reviews: the original LLM route, or
TypeSafe's Jev (System One). Jev is a decision model —
it answers typed questions with probabilities instead of writing prose — so one
review is a single HTTP call with no tool loop. Measured on this plugin's own case
set: ~0.9 s per review against 4–12 s for the LLM reviewer, and 16/16 agreement
with the expected policy outcomes. Everything downstream of the verdict — risk
gate, breaker, cache, ledger, Approvals tab — is shared by both engines.
To use Jev:
Store the key where the harness resolves credentials: the harness
credential settings, or $DSH_HOME/.env as TYPESAFE_API_KEY=…. The plugin
asks the credential store first (which itself layers the launch environment, the
managed store and .env files) and falls back to the process environment; it
re-resolves per review, so a rotated key applies to the next verdict with no
restart.
Turn the engine on in your profile patch. An id-targeted override replaces the whole row config, so restate any key you still want:
- id: approval-review
config:
reviewer:
engine: jev
jev:
allowEgress: true
Restart the harness (the engine is read at mount) and run
/approval-review status. It prints the engine, the access-mode gate and the
model, so a silent ledger always has a visible cause.
Two guards are deliberate: allowEgress: true is required because Jev is a
third-party endpoint and the evidence packet leaves the machine, and a selection
that does not fit the engine in force is reported rather than forwarded. Pick any
Jev model or LLM route from the Approvals tab — the choice carries its engine and
applies to that session only. The full contract, including what each engine can and
cannot do, is in Reviewer engines.
| Official seam | An approval/request answerer registered with prepend: true, so it claims a request ahead of the human UI answerer, and delegates everything else back to the chain. |
| Second-model review | Default direct receives the policy, an explicit evidence packet and a bounded local read-only inspector. Optional subagent/spawn offers read-only investigation but retains DSH preset inheritance. |
| No silent failure | A crashed, timed-out, truncated, or off-schema reviewer answer never becomes an approval: it yields the configured failure policy, which defaults to delegate — the request goes back to the human chain. Set onReviewerFailure: rejected for the fail-closed stance. |
| Rationale reaches the model | A denial's reason is appended to the refused tool result, with an explicit instruction not to pursue the same outcome through a workaround. An allow verdict rides the same channel (gated by recordAllowedVerdicts): the approval outcome is a closed vocabulary, so the tool result is the only place the plugin can write durably — without it the card can show that an action ran but never why. |
| Risk gate | An allow verdict above maxAutoAllowRisk does not auto-allow; it delegates to the human. |
| Circuit breaker | Consecutive and rolling-window denial thresholds, matching Codex's per-turn breaker, which stop the host turn after the triggering denial has been recorded by default. |
| Budgets | A per-turn cap on reviewer calls, so a loop cannot bill unlimited reviews. |
| One-shot override | /approval-review approve [n] records a human authorization for one retry. Bound to the same session, tool and byte-identical arguments; expires after five minutes by default and is not restored after restart. The reviewer still decides. |
| Fourth access mode | An 替我审批 ("approve for me") entry beside 仅可查看 / 工作区内修改 / 完全权限. It shares its sandbox and approval knobs with workspace-write on purpose — the difference is WHO answers — so the menu entry itself is the switch. PermissionPresetService.derive() checks the recorded selection first, which is what lets the two coexist and stay selected. |
| Verdict cache | Disabled by default because authorization and local state can change. Explicit opt-in is limited to direct review without recent transcript. |
| Failure budget | A per-turn cap on reviewer failures, so a broken reviewer cannot be retried without bound while the request waits. |
| Approvals tab | A conversation tab rendering every request with its verdict, routing policy, risk, rationale, safer-alternative suggestion, reviewer route, timing, and the live budget/breaker state, plus working on/off and one-shot-approve buttons. |
Risk and user authorization are assessed separately. Routine low/medium risk actions normally pass. High risk requires medium/high authorization, bounded scope and no prohibition; critical risk is denied. Escalation, outside-workspace paths and normal credential authentication are not intrinsically high risk. Missing evidence, truncated action arguments, timeouts and malformed answers delegate by default.
This is not a full Codex Guardian implementation. Default direct review can inspect local metadata, directory entries and text with at most four read-only calls; it delegates when evidence remains insufficient. The inspector permits workspace paths and exact outside paths named in the action, excludes credential stores, bounds reads to 16 KiB and listings to 100 entries, and never executes shell commands. Optional subagents still inherit DSH presets. The default breaker records the tool refusal, then cancels the host turn while retaining pending user input. Only host approval/request events are covered. Custom policyText replaces the semantic policy but cannot bypass the code-level critical-risk denial or high-risk authorization gate.
Upgrading changes the default reviewer mode, risk ceiling, breaker action and cache setting; explicit profile overrides still win. Recorded rationale retains its original language while UI labels follow the current locale.
Policy comparison and test evidence
No build step on install. The repository carries the built bundles (
lib/) andpackage.jsondeclares nopreparescript, because pnpm blocks a git dependency'sprepare/installbuild scripts (ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED) and that fails the entire install.dsh plugin add github:...therefore works with noallowBuildsentry in the profile'spnpm-workspace.yaml.You only build when working from a source clone:
pnpm build(andprepackruns it automatically before a publish).
# npm (published releases)
dsh plugin --profile <profile> add dsh-approval-review
# a local checkout
dsh plugin --profile <profile> add /path/to/dsh-approval-review
# a git pin
dsh plugin --profile <profile> add "github:LAwLi3tCoding/dsh-approval-review#<sha>"
Then restart and confirm the row composed:
dsh --profile <profile> --dump-config | grep -A6 'id: approval-review'
The desktop profile is managed by the Electron app and refuses dsh plugin; add
the dependency and bundle entry by hand there, or drive it from the app's plugin
manager.
All tunables live in the bundle's cordis.patch.yml row, so they are changeable
without touching code. An id-targeted override replaces the whole config row —
restate every key you still want, or the omitted ones silently return to their
schema defaults.
| Key | Default | Meaning |
|---|---|---|
enabled | true | Master switch. false mounts the plugin but claims nothing. |
enabledByDefault | true | Session-start default for the runtime switch. |
reviewerPreset | approve-for-me | The access-mode key that turns this plugin on. Empty means any access mode claims (for a deployment with no such entry). |
manageAccessModePreset | true | Write (or adopt) that access-mode entry in the profile patch while installed, and delete it on uninstall; false warns only and never writes configuration. See the access-mode entry's lifecycle. |
reviewTools | ['*'] | All tool approval requests by default. |
defaultPolicy | ai | Fallback routing policy for unmatched tools. |
rules | [] | Ordered {pattern, policy, field?, note?} regex rules, evaluated before the tool table. field is reason (default), toolName, or arguments. |
reviewer.engine | llm | Which engine answers: the original LLM reviewer, or TypeSafe's Jev. See Reviewer engines. |
reviewer.mode | direct | Isolated model call by default: no inherited parent prompt, history, skills or memory. Optional subagent can inspect the workspace. Applies to engine: llm only. |
reviewer.provider / .model | (inherit) | Reviewer route; unset inherits the calling agent's own route. |
reviewer.subagentProvider | spawn | Optional subagent backend; spawn omits parent history but still inherits the host preset. |
reviewer.inspectLocalState | true | Enable the bounded local inspector in direct mode. |
reviewer.tools | [read, glob, grep] | The reviewer child's tool allow-list. An empty list falls back to the read-only default rather than the parent's whole face. |
reviewer.timeoutMs | 120000 | Hard deadline for one reviewer call. A slow route plus a reasoning reviewer can take ~50s; a deadline that expires mid-review becomes a failure-policy outcome (a delegation to the human by default), not a verdict. |
reviewer.maxTokens | (model route default) | Optional output cap; omitted by default so the adapter/model configuration applies. |
reviewer.temperature | 0 | Sampling temperature. |
reviewer.policyText | (shipping policy) | Replaces the ruling policy text. |
reviewer.guidance | (none) | Extra deployment guidance appended after the policy. |
reviewer.argumentMaxChars | 4000 | Per-string argument cap. |
reviewer.argumentsBudgetChars | 16000 | Whole-argument-document cap; 0 disables. |
context.turns | 2 | Prior turns of transcript evidence; 0 omits recent transcript; selected original/latest user intent is still supplied. |
context.maxChars | 6000 | Transcript character budget. |
context.includeAssistant | true | Include assistant messages in the transcript. |
context.includeToolActivity | true | Include tool calls and results. |
maxAutoAllowRisk | high | High risk also requires medium/high authorization and bounded scope; critical risk is always denied. |
onRiskExceeded | delegate | allow / delegate / deny above that ceiling. |
onUncertain | delegate | Reviewer reported it could not decide. |
onReviewerFailure | delegate | Reviewer crashed, timed out, or answered off-schema. Defaults to delegating: a reviewer that could not run is an infrastructure problem, not a verdict — set rejected for the fail-closed stance. |
budget.maxReviewsPerTurn | 20 | Reviewer calls per open turn. |
budget.onExhausted | delegate | delegate / deny once spent. |
maxFailuresPerTurn | 10 | Reviewer failures per open turn before requests delegate. |
verdictCache.ttlMs | 0 | Disabled by default. Opt-in only for direct mode with context.turns=0 and inspectLocalState=false; key includes session, user evidence and model. |
verdictCache.maxEntries | 256 | Cached fingerprints before oldest-eviction. |
circuitBreaker.consecutiveDenials | 3 | Consecutive denials that trip the breaker. |
circuitBreaker.windowDenials | 10 | Denials within windowSize that trip it; 0 disables. |
circuitBreaker.windowSize | 50 | Rolling window size. |
circuitBreaker.action | stop | Stop the host turn after recording the refusal; delegate / deny remain available. |
override.ttlMs | 300000 | How long an /approval-review approve stays usable; 0 never expires. |
override.maxPending | 10 | How many recent denials the override can address. |
reasonMaxChars | 2000 | Cap on any reason string the plugin emits. |
feedReasonToModel | true | Append the rationale to the refused tool result. |
recordAllowedVerdicts | true | Append the allow verdict to the accepted tool result, so the card can show why an action was allowed. Costs one short marker block in the model context per auto-allowed call. |
language | auto | Prose language this plugin emits: /approval-review command output and the reviewer's reason/suggestion fields. auto follows the harness language setting (Settings → General → Language), en/zh pin it. Resolved per call, so a switch applies to the next command and the next verdict. Boundaries: the decision/risk enums stay English tokens (the parser validates them), and text already recorded in the transcript — an earlier verdict's prose, an earlier command's output — is never rewritten. |
The "Approve for me" entry lives in the permission row's preset table. An
id-targeted override replaces that whole row, so any later layer that restates
the table — typically a profile patch pinning its own defaultPreset — silently
drops the entry this package ships. The menu then lists three modes, the plugin's
gate key is not in the catalog, and it claims nothing while the log says nothing.
Two guards fix that:
permissionPresets.catalog()). A missing gate key is named in the startup
log, and the Access-mode catalog line of /approval-review status keeps
reporting the conclusion — missing, written, or only fixable by hand.manageAccessModePreset: true, the default). Through the
configEditor service the plugin writes the missing entry into the profile
patch — the layer no later override can shadow — and adopts an identical
entry a human already pasted there instead of writing a duplicate. On uninstall
it deletes that entry again: installing adds the menu option, uninstalling
removes it, and no stale option is left behind.Ownership stays narrow, so the plugin never touches somebody else's preset:
dsh.profile.bundles. A normal app
exit unmounts plugins too, and there the bundle is still listed, so the entry
survives a restart as it should.configEditor) is a warning, not a mount failure, and
/approval-review status shows the reason.For a deployment whose configuration is read-only, set
manageAccessModePreset: false: the self-check still warns, and nothing is ever
written.
llm and jevreviewer.engine chooses who answers a review. llm (the default) is the original
path: one model call — or a read-only subagent — reads the evidence packet and
returns the verdict JSON. jev posts the same packet to
TypeSafe's System One endpoint and reads typed
answers back, with the decisions (prohibition hit, uncertainty, bounded scope)
applied in code. Everything downstream of the verdict — the risk gate, the
breaker, the cache, the ledger and the card — is shared by both engines.
Jev never touches DSH's model routes: it is a direct HTTP call with a bearer key,
so it does not appear in the model picker and needs no provider registration. The
typesafe/<model> value shown in the ledger is a label the plugin synthesizes for
its own audit record, not a DSH route; /approval-review model <id> still works
and overrides the model name or version sent to TypeSafe.
A selection from the picker carries its engine: typesafe/<model> reviews with
Jev, <provider>/<model> reviews with that LLM route, a bare <model> keeps the
engine already in force and replaces only the model, and default returns to the
deployment's own reviewer. That is what makes every listed row mean something —
including switching a Jev deployment back to an LLM for one session, which the
header pill and the ledger both follow. Two limits hold: a session may switch to
Jev only when the deployment acknowledged egress (jev.allowEgress), and the Jev
endpoint receives a bare model name, so a value that still contains / after the
typesafe/ marker is stripped is reported and ignored rather than forwarded.
| Key | Default | Meaning |
|---|---|---|
reviewer.jev.endpoint | https://api.typesafe.ai/v1/systemone | Where the evidence is POSTed. Point it at your own gateway if the packet must not go upstream directly. |
reviewer.jev.model | jev-latest | Model or alias. The response reports the version that answered (jev-1.13.0), and that is what the ledger records. |
reviewer.jev.apiKeyEnv | TYPESAFE_API_KEY | Environment variable holding the key. The key is never read from config, never logged, never written to the audit record. |
reviewer.jev.timeoutMs | 8000 | Deadline for the single HTTP call; independent of reviewer.timeoutMs. |
reviewer.jev.permitProbMin | 0.6 | Top probability below which the permit answer counts as uncertain. |
reviewer.jev.prohibitedAt | 0.5 | Probability at which any of the four prohibitions refuses outright, regardless of authorization. Deliberately asymmetric: a false refusal is cheaper than a false allow. |
reviewer.jev.scopeBoundedAt | 0.5 | Probability at which the scope answer counts as bounded. |
reviewer.jev.allowEgress | false | Must be true for the plugin to mount. Otherwise loading fails and names the endpoint the evidence would reach. |
reviewer.jev.rubric | {} | Per-question instructions overrides keyed by question id (see src/jev-questions.ts). |
What differs in practice:
uncertain and
follows onUncertain (a human prompt by default) instead of being investigated.
reviewer.mode and reviewer.inspectLocalState do not apply.reason is composed in code from the answers — for example
Denied: prohibition "disclosure of secrets or private data" (0.97); risk critical; authorization unknown; permit 1.00 —
in the configured output language. reviewer.policyText does not apply: the
ruling policy lives in the per-question rubric.allowEgress
acknowledges. Redaction is unchanged (key-name based, plus a best-effort pass
over unparsable payloads); it does not scrub secrets out of free text.