LAwLi3tCoding/dsh-approval-review ↗★ 0
dsh-approval-review
Codex-style auto-approval for DeepSeek Harness. An independent reviewer decides allow/deny, fail-closed, with a rationale for every decision. Engine: an LLM route, or TypeSafe's Jev (`reviewer.engine`). · DSH 插件:独立复核器裁决,引擎可选 LLM 路由或 Jev。 适合需要对 Agent 工具调用进行安全合规审查与自动决策的系统。
설치
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:LAwLi3tCoding/dsh-approval-reviewConfiguration
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. |
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. |
Reviewer engines: llm and jev
reviewer.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/ value shown in the ledger is a label the plugin synthesizes for
its own audit record, not a DSH route; /approval-review model still works
and overrides the model name or version sent to TypeSafe.
A selection from the picker carries its engine: typesafe/ reviews with
Jev, / reviews with that LLM route, a bare `` 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:
- One HTTP call per review, no tool loop. Jev cannot inspect local state, so a
request whose decisive fact is not in the evidence becomes
uncertainand followsonUncertain(a human prompt by default) instead of being investigated.reviewer.modeandreviewer.inspectLocalStatedo not apply. reasonis composed in code from the answers — for exampleDenied: prohibition "disclosure of secrets or private data" (0.97); risk critical; authorization unknown; permit 1.00— in the configured output language.reviewer.policyTextdoes not apply: the ruling policy lives in the per-question rubric.- The evidence packet leaves the machine, which is what
allowEgressacknowledges. Redaction is unchanged (key-name based, plus a best-effort pass over unparsable payloads); it does not scrub secrets out of free text. - The key is resolved through the harness credential store first, then the
environment. Store it in the credential settings, in
$DSH_HOME/.env, or export it before launch: the credential seam already layers those sources (launch environment → managed store → project.env→ harness-home.env) and re-resolves per request, so a rotated key applies to the next verdict without a restart. A GUI-launched harness never runs your shell startup files, which is why the store — not~/.zshenv— is the place that works.
What happens when Jev is not configured, or only half configured:
| State | Result |
|---|---|
reviewer.engine left at llm (the default) | The LLM reviewer runs. No request reaches TypeSafe, no key is needed, and no Jev warning is logged. |
engine: jev without jev.allowEgress: true | The plugin refuses to mount, naming the endpoint the evidence would reach. Nothing is reviewed automatically. |
engine: jev, egress acknowledged, but no key resolvable from the store or the environment | The plugin mounts and warns at startup when no credential store is mounted; with a store, the first review fails with a missing-credential message instead. Either way the failure follows onReviewerFailure — delegate by default, so the request reaches a human instead of being silently allowed. After maxFailuresPerTurn failures the turn stops consulting the reviewer at all. |
engine: jev with mode: subagent, or a non-https endpoint | The plugin refuses to mount. |
Key rejected (401/403), rate limited (429), 5xx, timeout, non-JSON, or a missing answer | Recorded as a reviewer failure; no retry and no partial answer is acted on. |
Session access mode is not reviewerPreset | The plugin claims nothing, by design — the ledger stays empty and /approval-review status names the gate. |
Enabling Jev on a fresh install
-
Store the key where the harness can resolve it: the harness credential settings, or
$DSH_HOME/.envasTYPESAFE_API_KEY=…. Both are read per review;process.envis the fallback for a deployment that mounts no credentials row. -
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 gate and the model, so a silent ledger always has a visible cause.
Verify connectivity before enabling it — the live probe sends synthetic evidence only, never repository content:
export TYPESAFE_API_KEY=… # the same key the plugin resolves at review time
npx vitest run tests/jev-live.test.ts tests/jev-policy-live.test.ts
Tool policies
ai— this plugin's reviewer decides.allowed-onceorrejected.human— delegate withnext()to the rest of the answerer chain: the ordinary approval prompt. The plugin never short-circuits it.never— deterministicrejectedwith an explanatory marker, no reviewer call and no prompt. The hard-disable stance for a tool family.
edit is deliberately not in the default reviewTools: in-place modification
of an existing file is the highest-consequence routine action, so it keeps the
human prompt until a deployment decides otherwise.
Example: stricter deployment
- insert:
- id: approval-review
name: dsh-approval-review
config:
reviewTools: ['bash', 'pwsh', 'write', 'edit']
defaultPolicy: human
rules:
- pattern: '(?i)(rm\s+(-[a-z]+\s+)*/|git\s+push\s+--force)'
policy: never
note: destructive
- pattern: 'curl|wget|nc\s'
policy: ai
field: arguments
reviewer:
model: ''
timeoutMs: 30000
maxAutoAllowRisk: low
onRiskExceeded: delegate
circuitBreaker: { consecutiveDenials: 2, windowDenials: 5, windowSize: 20, action: deny }