drscrewdriver/dsh-perm-gate

drscrewdriver/dsh-perm-gate

P0–P4 确定性优先权限门控,含 Permissive 档位、学习沉淀与审批历史 UI;凭据/受保护路径硬拒绝,内部只读工具自动放行。

安全与权限

dsh-perm-gate

  • English README
  • 中文 README
  • 日本語 README
  • 한국어 README
  • Installation guide
  • 中文安装指南
  • 日本語インストールガイド
  • 한국어 설치 안내
  • Changelog
  • 日本語 changelog
  • 한국어 changelog
Compatibility note: v2.0.0 ships ja / ko dictionaries, but official DSH exposes
only zh / en through LocaleRuntime (LOCALE_IDS = ["zh", "en"]). On stock DSH,
selecting ja / ko fails with locale "" is not registered. Use a DSH fork that
updates LOCALE_IDS (locale-settings.ts) and LOCALES labels (client/index.ts), then
rebuild.
▼ DSH version compatibility
Two DSH lines are served from two long-lived branches, each with its own version
series, engines.dsh, and npm dist-tag (release layout):
| DSH version | Branch | Version | npm tag |
| --- | --- | --- | --- |
| 0.1.0-rc.7 ~ 0.1.1-rc.x | legacy | 1.x | @legacy |
| 0.1.2-alpha.1+ (incl. 0.1.5-rc.2) | main | 2.x | @latest / @dsh-0.1.2 (@2.x is a range) |
The series number tracks the DSH line (1.x = DSH ≤ 0.1.1, 2.x = DSH 0.1.2+),
and the majors fence each other: a ^1.x install never resolves a 2.x release and
vice versa. engines.dsh states the same split but DSH never reads it — the ranges
and dist-tags are what hold an old DSH on 1.x.
@deepseek-ai/dsh-client-runtime was removed at 0.1.2-alpha.1 — it did not
merely move. The legacy line still reaches ctx.slots through it; main gets
the same declaration from @deepseek-ai/dsh-client-ui-renderer/client. Two
version-sensitive seams are handled by capability probes rather than version
checks: (1) settings registration uses register, which exists on both lines
(installSection is an addition, not a replacement); (2) effectivePolicy is a
private method of the user-approval service on both, so it is read behind a
typeof probe and degrades to “policy unknown” when absent or throwing.

Version 2.6.0 — see the Changelog.

A single, self-sufficient, deterministic-first, fail-closed permission gate for DeepSeek Harness.

dsh-perm-gate decides every tool call through a fixed priority chain:

Stage · Decision · What it is P0 · deny · deterministic hard-deny: credential material, protected-path mutation, dangerous shell P1 · allow · a precise, bounded session grant P2 · deny/allow/ask · static rule chain: blacklist first, then allow, then ask P3 · allow/deny/ask · optional LLM semantic classifier (default off) P4 · ask · official approval seam

Strictly fail-closed: a P0 decision is never overridden by a grant, a rule, the classifier, or a human.

  • Supported DSH hosts: 0.1.0-rc.2 0.1.0-rc.3 0.1.0-rc.6 0.1.0-rc.7 0.1.0-rc.8 0.1.1-rc.1 0.1.1-rc.2 0.1.2-rc.1 0.1.5-rc.1 0.1.5-rc.2 0.1.5-rc.3 0.1.7-rc.1 0.1.7-rc.2 0.2.0-rc.1 0.2.0-rc.2 0.2.1-rc.1 0.2.1-rc.2 0.2.1-rc.3 (18 rc releases across lines 0.1.0 / 0.1.1 / 0.1.2 / 0.1.5 / 0.1.7 / 0.2.0 / 0.2.1)
  • Runtime-verified: 0.2.0-rc.2 — evidence in .compat-results/

Why

The DSH safety ecosystem splits this across several plugins (dsh-permission-rules, dsh-auto-mode, dsh-auto-review, dsh-movein-permissions). dsh-perm-gate merges the gate + approval + (optional) classifier into one package, with one audit trail and no cross-plugin version coupling.

Features

  • Command whitelist / blacklist — matched on an argv decomposition (not a raw string),

with recursive descent into sh -c/bash -c, pipeline detection, redirect-target checking, and recursive/force (rm -rf) recognition.

  • Deny priority — a matching deny rule wins over any allow rule.
  • Session grants — precise (tool, canonical fingerprint) grants with TTL + maxUses;

re-running with a different target never reuses authority. Sub-agents inherit but cannot mint.

  • Pure-function rule engine — glob/regex compilation with a ReDoS bound, loud fail on

malformed rules, and source-hash compile caching.

  • Audit — every decision is logged as an {ignorable:true} event with its callId; the

model-visible reason matches the recorded outcome.

  • 自动审查 tier (permissive, plus permissive-full) — an independent approval mode

(separate from read-only, full-access and whitelist tiers) that is neither "auto-approve" nor blanket trust. Front-end exposes a single switch (permissive); the four backend strategies are combinable and driven by plugin settings — still fail-closed against P0. The permission picker and the settings row both show it under the product label 自动审查, with no icon. The permissive-full variant keeps the identical approval behaviour but drops the built-in file sandbox, which otherwise denies the named pipes git clone / Cygwin / ConPTY need.

  • Sandbox-escalation auto-answer (trustEscalation) — a sandbox escalation is asked from

*inside* the shell / pwsh / edit tool body, after tools/pre-execute, so the gate never saw it and a call it auto-allowed still prompted you to approve the widening. With this strategy on, the exact call the gate cleared (matched by callId) is answered here instead.

  • Risk-graded `llmAssist` — a custom OpenAI-compatible LLM grades each ask as

safe / risky:; hard categories (deletion, credential, remote, system, bulk) always ask, neutral enters verdict learning, and every failure stays fail-closed.

  • Verdict learning — neutral-risk asks that the human approves and that actually execute

count up; after the threshold, the *exact same operation* (fingerprint-matched) auto-allows.

  • Decision event feed — every decision is appended to a JSONL feed and surfaced by the

browser half as a notice strip above the conversation input plus an approval-records tab (newest first) in the conversation view.

A preset deny-keyword blacklist (inherited from dsh-approval-gate's DEFAULT_DENY_KEYWORDS: rm -rf, push --force, drop table, mkfs, git reset --hard, docker system prune, …) vetoes any call whose text contains a keyword — case-insensitive substring, applied before whitelist, grants and LLM. It is editable as a list in the settings card (preset entries are tagged, and a one-click restore brings the preset back); unset or empty applies the preset — the blacklist never silently turns off.

Install

Requires an existing DeepSeek Harness installation.

dsh plugin --profile web add dsh-perm-gate

Full install, upgrade, migration and troubleshooting steps live in the Installation guide — also available in 中文 / 日本語 / 한국어.

Configuration

AppContainer sandbox (Windows; beta)

dsh-perm-gate can run gate-allowed shell commands inside a Windows AppContainer instead of handing them back unconfined. The container token is created with zero network capabilities, so non-loopback egress is blocked by Windows Firewall (WFP) itself — not by an advisory environment variable. The container SID is OS-derived from the fixed name dsh-perm-gate.ac, so the one-time CheckNetIsolation LoopbackExempt (which lets the container reach 127.0.0.1) is a per-machine step, not a per-session one. Egress is therefore possible only through the network proxy:

  • sandboxEnabled (default false) — master switch. When on, every auto-allowed shell call is

rewritten into the launcher invocation; a human-approved call is out of scope (the user approved it consciously). If the launcher cannot be built or the OS rejects the container launch, the call is denied (fail-closed) with reason sandbox: … — never run unconfined.

  • sandboxMode — workspace-write (default; workspace gets Modify + private temp FullControl,

system dirs stay readable through the stock ALL APPLICATION PACKAGES ACEs) or read-only.

  • sandboxProxy — the proxy parameter: empty (default) follows the builtin filtered proxy's

actual 127.0.0.1: when networkEnabled; an explicit URL must be loopback-bound (the container cannot reach anything else) and destination filtering is that proxy's job.

  • sandboxLoopback — exempt (default; attempt the one-time exemption, warn with the exact

elevated command if denied) or off.

The launcher is a small C# program shipped as source (assets/appcontainer-launcher.cs) and compiled on first use by the .NET Framework compiler every Windows carries (csc.exe); the plugin itself keeps zero native dependencies. Verify a machine end-to-end with:

node scripts/smoke-appcontainer.mjs

Known boundary: on Windows 11 25H2+ builds where the classic AppContainer creation APIs have been removed (verified on 26200) and the mxc replacement is not yet exposed (26600+), the launcher fails closed at the kernel step; the sandbox then denies sandboxed calls with a clear reason instead of running them unconfined. On Windows 10 / 11 ≤ 24H2 the documented classic path applies.

Add the plugin to cordis.yml:

- id: dsh-perm-gate
  name: dsh-perm-gate
  config:
    rulesFile: ./permissions.yaml   # optional; defaults to $DSH_HOME/perm-gate/rules.yml
    dshHome: $DSH_HOME              # root pinned for protected-target checks
    defaultAction: ask              # allow | ask | deny
    gatePresets: [permissive, permissive-full]   # tiers where the gate is active (default)
    sessionSweep: true              # hourly cleanup of archived/dead sessions' gate data

Session sweep

On startup and every hour the gate reads DSH's workspace store ($DSH_HOME/storages/workspace.json, read-only) and classifies every session it holds authorization-chain data for. Sessions DSH has archived (global.archivedSessionIds) or no longer tracks at all have their decision events dropped from $DSH_HOME/perm-gate/events.jsonl and their pre-change snapshot files deleted from $DSH_HOME/perm-gate/snapshots/ — history the review page can no longer reach, for data the harness itself considers gone. Live sessions are untouched, unattributable rows (empty session id) are never deleted, and any failure is fail-open: the round is skipped and retried an hour later. Set sessionSweep: false to disable; workspaceStoreFile overrides the store path. Restoring an archived session does not restore its swept history.

Rules file

permissions:
  defaultAction: ask
  deny:
    - command: [rm#recursive]
      reason: no recursive rm
    - command: [ssh]
      reason: no direct ssh
    - paths: [.dsh/**]
      reason: protect harness metadata
  allow:
    - command: [pnpm, node]
      reason: dev tools
    - command: [curl, wget]
      args: ["https://*.example.com/*"]
      reason: allowed endpoint
  ask:
    - command: [bash, sh]
      reason: ask shells

A command entry word#flag matches the command word (word) with the modifier recursive or force — so rm#recursive matches rm -rf, env rm -rf, and sh -c "rm -rf /".

Network policy (opt-in)