Bundle
dsh-permission-gate
A DeepSeek Harness (DSH) plugin that adds an AI-adjudicated 'smart' approval mode: safe operations are auto-allowed via an isolated side-channel LLM judge, everything else falls back to the normal human approval popup.
- Source
- masquerator-coder
- License
- Apache-2.0
- Updated
- Updated 18 hours ago
Readme
# dsh-permission-gate
A [DeepSeek Harness](https://deepseek-harness.github.io/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 an `allow` below 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-access` by 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 yields `danger` and goes to the human. You can override this set with `dangerRules` in `cordis.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):
```bash
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's `cordis.patch.yml` restates the three built-in presets (`read-only`, `workspace-write`, `danger-full-access`) unchanged and adds `smart`. 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:
```yaml
- 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 against `toolName` and `reason`. Set to `[]` to disable the local gate entirely.
## Development
```bash
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
Install
dsh plugin --profile web add github:masquerator-coder/dsh-permission-gate
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-permission-gate from the hub
- This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.