Skip to content
dsh.fish
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

  • 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.
Source