dsh-permission-gate
A DeepSeek Harness (DSH) plugin that adds an AI-adjudicated smart approval mode alongside the built-in read-only / workspace-write / danger-full-access presets.
When smart mode is active, operations that need approval are side-channelled to an isolated LLM judge instead of immediately prompting a human. Safe operations are auto-allowed; anything that is not confidently safe — a deterministic-danger rule hit, an unsure model, a timeout, an unparseable response, or an internal exception — falls back to the normal human approval popup.
The judge call never disturbs the running session: it uses its own provider/model route, its own AbortSignal and deadline, and its own token budget, outside the agent loop.
Safety contract: smart mode only ever auto-allows. It never auto-rejects. A rejected/dangerous/uncertain verdict always lands in front of a human. Auto-allowing does not bypass the sandbox: the smart preset keeps
sandbox: workspace-write, so an auto-allowed action still runs inside that confinement.
How it works
The built-in permission presets are a bundle of two enforcement knobs: sandbox mode and approval policy. smart keeps approval: ask (so human approval is always reachable as the fallback) and sandbox: workspace-write.
This plugin installs a host-side, prepended, global answerer on the approval/request waterfall — the same documented extension point dsh-user-approval uses to route decisions to answerers. Because it is registered with { global: true, prepend: true }, it runs before the browser UI answerer, so it can claim a request with allowed-once before the popup would appear, or defer with next() to let the popup show.
tool needs approval
──▶ ApprovalService.request ──▶ approval/request waterfall (agent-scoped)
──▶ [prepend, global] dsh-permission-gate answerer
├─ session preset is NOT smart ──▶ next() (inert)
├─ deterministic-danger rule hit ──▶ next() (human)
└─ else: isolated LLM judge (own route/deadline/signal)
├─ allow & confidence ≥ threshold ─▶ 'allowed-once' (auto-release)
└─ unsure / danger / timeout / malformed / exception ─▶ next() (human popup)
──▶ [next] dsh-ui-approval human popup → allow/reject
Decisions that fall back to next() are exactly the ones the human popup handles:
- Deterministic-danger rule — a configured substring pattern matched against the tool name or the ask reason (e.g. credentials, destructive filesystem, force-push, genuine privilege escalation).
- Model unsure — the judge answered
unsure/danger/unknown, or anallowbelow the confidence threshold. - Timeout — the judge call exceeded
judgeTimeoutMs. - Malformed response — the model output could not be parsed into a trustworthy JSON verdict.
- Internal exception — the LLM stream/adapter threw, or no judge model route was resolvable.
About out-of-workspace operations: the default danger rules no longer short-circuit on the phrase
danger-full-accessby itself. Low-risk out-of-workspace file access (writing/appending/reading confirmed non-secret files) is handed to the judge, which conservatively allows it; deleting unconfirmed data, reading credentials, executing external code, installing global packages, or network exfiltration still yieldsdangerand goes to the human. You can override this set withdangerRulesincordis.yml.
Everything is fail-safe: any of the above returns allowed-once only for a confident allow, and every failure goes to a human.
Install
Add the bundle to a profile's bundle stack (so the smart preset row and the answerer are registered together):
dsh plugin add dsh-permission-gate
# or git source (pin a commit):
# dsh plugin add "github:owner/repo#<sha>&path:dsh-permission-gate"
then restart DSH. After install, the permission selector shows 智能审批 (smart) as a fourth option.
Bundle rows override by
id: this package'scordis.patch.ymlrestates the three built-in presets (read-only,workspace-write,danger-full-access) unchanged and addssmart. If your deployment customizes the preset table elsewhere, that layer must come before this bundle so this override wins (or restate your own table here).
Activation
Smart adjudication is active only while the session's effective preset is smart. Under any other preset the plugin is inert (every request is deferred to the normal answerer). Select smart from the Access chip / /permission smart, or set defaultPreset: smart in the permission row.
Configuration
All keys are Schemastery-validated and changeable from cordis.yml/--patch. Example:
- insert:
- id: permission-gate
name: dsh-permission-gate
config:
provider: deepseek # explicit provider route; must pair with model.
model: deepseek-chat # explicit judge model.
judgeTimeoutMs: 15000 # deadline for one judge call before falling back to human.
maxTokens: 256 # judge output-token cap.
minConfidence: 0.85 # min confidence for an allow to auto-release.
# Optional override of the built-in safety-adjudication system prompt.
systemPrompt: "..."
# Deterministic-danger rules evaluated before any LLM call. A match → human.
dangerRules:
- label: secrets
patterns: ["ssh", ".env", "api.key"]
- label: destructive
patterns: ["rm -rf", "format "]
provider/model: if omitted, the judge uses the session's last logged model route (request/header). If neither an explicit pair nor a logged route is available, the request is treated as an internal-exception fallback (human decides) — fail-safe.dangerRules: substring patterns, case-insensitive, matched againsttoolNameandreason. Set to[]to disable the local gate entirely.
Development
pnpm install
pnpm typecheck # tsc --noEmit
pnpm test # vitest (judge pure functions + end-to-end answerer with stubbed LLM)
pnpm build # tsdown → lib/
tests/answerer.test.ts mounts the real ApprovalService with a stubbed llm/permissionPresets and verifies: inert under non-smart presets, auto-allow on a confident allow, and each fallback path (unsure / danger / danger-rule / malformed / exception / no-route) deferring to a human answerer.
License
Apache-2.0
No comments yet. Be the first to write one.