Workflow & Automation

dsh-tool-policy

drifter-yh/dsh-tool-policy · ★ 3

drifter-yh

Declarative deny-by-default tool policy plugin for DeepSeek Harness

License
MIT
Added
2026-08-23
Maintenance
active
Install kind
github
npm
@drifter-yh/dsh-tool-policy

No manifest check is available; listing does not imply compatibility or security review.

安装

dsh plugin --profile web add github:drifter-yh/dsh-tool-policy

GitHub 源码安装:首次需按提示配置 allowBuilds 构建授权后重试

For install and environment setup, plus plugin development tutorials, see DSH community docs ↗

README

Source README · body: en · Read Chinese README

Table of contents
  1. Why this is needed
  2. Security model
  3. What this does not do
  4. Installation
  5. GitHub installation
  6. Harness profile bundle
  7. Quick Start
  8. Routing presets
  9. Configuration
  10. Policy decision trace
  11. Recipes
  12. Architecture
  13. Example
  14. Compatibility with DeepSeek Harness
  15. Current limitations
  16. Roadmap
  17. Implemented
  18. Next
  19. Future
  20. Development
  21. License

English | 简体中文

A DeepSeek Harness plugin that applies allow, ask, or deny rules to tool calls before they execute.

It provides a declarative, deny-by-default policy layer for built-in, third-party, and MCP tools, while reusing Harness's existing approval and sandbox mechanisms. It is a per-call policy layer, not a capability sandbox.

Community plugin. Not affiliated with or maintained by DeepSeek AI.

Repository: Drifter-yh/dsh-tool-policy

Why this is needed

DeepSeek Harness already has strong primitives for tool execution: sandbox policy, one-shot approval, cooperative timeouts, provider retries, repeat-call reminders, and session telemetry. What is missing is a deployment-owned policy layer that applies the same rule vocabulary to every tool, including third-party and MCP tools.

Typical uses include:

  • allow selected tool namespaces or families such as read_*;
  • require human approval for MCP or other external tools such as mcp__*;
  • deny a known destructive command pattern before the matched tool body starts;
  • run a deny-by-default tool-call allowlist for unattended jobs;
  • keep sensitive argument values out of policy feedback messages.

A typical call path looks like this:

Agent wants to call a tool
          |
          v
    dsh-tool-policy
          |
   +------+------+
   |      |      |
 allow   ask    deny
   |      |      |
continue Harness stop before
pipeline approval tool body

The plugin is intentionally not an audit logger or approval implementation. The Harness already owns those seams.

Security model

The plugin operates on individual tool calls. Its rules match observable tool names and optional argument patterns before the tool body runs:

  • Harness sandbox — capability enforcement: Can this agent perform this class of operation at all? Harness sandboxing and runtime isolation are the layers responsible for enforcing capabilities such as filesystem writes or deletes, network access, and process execution.
  • dsh-tool-policy — per-call policy / routing: Should this particular known tool call be allowed, denied, or escalated? A matching deny prevents that call from executing; it does not revoke the underlying capability.
  • Harness Approval — human escalation verdict: Should an escalated ask call receive a one-shot human verdict?

A rule that matches one shell argument pattern, such as rm -rf /foo, only governs that call shape. A different tool or command sequence may produce the same effect. Tool policy and capability sandboxing are therefore complementary layers; production deployments should combine policy routing with a restrictive Harness sandbox.

What this does not do

dsh-tool-policy does not implement sandboxing, capability enforcement, shell semantic analysis, or equivalent-operation detection. A deny rule makes the matched call unavailable, not destructive behavior impossible in general. It does not rewrite arguments or execute tool bodies.

Installation

[!IMPORTANT] The official npm package for this project is @drifter-yh/dsh-tool-policy.

The unscoped npm package dsh-tool-policy is maintained by a different third-party project and is not published from this repository.

Use the scoped package name when installing from npm.

The recorded validation baseline uses the public Harness 0.1.1-rc.2 package line:

pnpm add --save-exact @drifter-yh/dsh-tool-policy @deepseek-ai/cordis@4.0.1 @deepseek-ai/dsh-tools@0.1.1-rc.2

The Harness packages are peer dependencies so the host controls the runtime version. @deepseek-ai/schemastery is installed as the plugin's normal runtime dependency. This package is tested against the public 0.1.1-rc.2 registry artifacts.

The example pins the two direct Harness peers, but does not reproduce the full CI lockfile. See CONTRIBUTING.md for the locked development baseline.

GitHub installation

The upstream profile-plugin documentation supports installing a TypeScript bundle directly from GitHub:

dsh plugin --profile my-profile add github:Drifter-yh/dsh-tool-policy#e6f43c255345a6f6bbab66ee8c053a2e5457e3c7

Git installs fetch source, so this package's prepare script runs only the standalone tsdown build needed to create dist/. With pnpm 10 or newer, add the package to the profile's pnpm-workspace.yaml build allowlist if pnpm reports that the prepare script is blocked, then retry:

allowBuilds:
  '@drifter-yh/dsh-tool-policy@git+https://github.com/Drifter-yh/dsh-tool-policy.git#e6f43c255345a6f6bbab66ee8c053a2e5457e3c7': true

Review and pin the Git commit before allowing install-time code execution. prepare does not run tests or depend on a DeepSeek Harness checkout.

For local development, use the ordinary package-manager workflow from a clean clone: pnpm install.

Harness profile bundle

The package also follows Harness's official profile-bundle contract: its package.json declares dsh.bundle.patch, and the published package contains cordis.patch.yml. Install it into a profile with:

dsh plugin --profile my-profile add @drifter-yh/dsh-tool-policy

That activates one tool-policy row with defaultDecision: deny and no rules. Before starting an agent, configure the row in $DSH_HOME/profiles/my-profile/cordis.patch.yml:

- id: tool-policy
  config:
    defaultDecision: deny
    rules:
      - tool: 'read_*'
        decision: allow
      - tool: 'bash'
        decision: ask
        reason: 'Shell execution requires approval.'

Harness profile patches target the row by id and replace its whole config; repeat every configuration field you want to keep. The bundle patch is only a composition layer: the plugin remains usable as a direct Cordis entry.

Quick Start

Add the community plugin directly to a Cordis composition. This example is explicitly deny-by-default and allows only tools matched by read_* unless another rule handles them:

- id: tool-policy
  name: '@drifter-yh/dsh-tool-policy'
  config:
    defaultDecision: deny
    rules:
      - tool: 'read_*'
        decision: allow
      - tool: 'bash'
        decision: ask
        reason: 'Shell execution requires approval.'
      - tool: 'mcp__*'
        decision: ask
        reason: 'External tool calls require approval.'
      - tool: 'delete_*'
        decision: deny
        reason: 'Delete operations are disabled in this deployment.'

The plugin is fail-closed at the tool-call layer when mounted: the default decision is deny, so only explicitly allowed calls run. Set defaultDecision: allow only when intentionally deploying a targeted or advisory policy.

Routing presets

Two optional starters expand into ordinary rules before compilation:

  • read-only: { tool: 'read_*', decision: 'allow' }.
  • mcp-approval: { tool: 'mcp__*', decision: 'ask' }.

Both leave the default decision at deny unless you explicitly set defaultDecision. They match names only: a tool called read_* is not necessarily read-only, and non-MCP external tools are not covered by mcp__*. Review the actual tools and adjust the rules for your deployment.

preset: mcp-approval
rules: # evaluated first: overrides the preset for this tool
  - tool: mcp__trusted__read
    decision: allow
appendRules: # evaluated after the preset
  - tool: read_record
    decision: allow

The complete order is rules, the preset's rules, then appendRules, with the first match winning. An appended rule cannot override an earlier match. Without a preset, existing rules behavior is unchanged. Trace rule numbers refer to this expanded order. In TypeScript, expandPolicy(config) (exported from both package entry points) validates and returns the plain, JSON-serializable { defaultDecision, rules } for inspection or storage; compilePolicy expands the same configuration before evaluation.

These presets route individual calls; they do not replace Harness sandboxing or approval configuration. Configure the Harness approval service and handler for ask, and use a restrictive Harness sandbox to enforce capabilities.

Configuration

defaultDecision: deny # deny (default), ask, or allow
trace: false # emit an argument-free decision trace through the Cordis logger
rules:
  # First matching rule wins.
  - tool: 'bash'
    decision: deny
    reason: 'Destructive shell commands are disabled.'
    argument:
      path: /command
      contains: 'rm -rf'

  - tool: 'record.update'
    decision: deny
    reason: 'System records are immutable.'
    argument:
      path: /scope
      equals: system

  - tool: 'safe_*'
    decision: allow

  - tool: '*'
    decision: ask
    reason: 'Unlisted tools require approval.'

tool is an anchored name pattern with one wildcard, *. Other regular-expression metacharacters are treated literally. argument.path is an RFC 6901 JSON Pointer into parsed tool arguments. A condition uses exactly one of equals (JSON scalar equality) or contains (non-empty substring on a string). Rule order is explicit and deterministic.

Decision semantics:

  • deny returns a normal Harness tool error before the body runs;
  • ask returns { kind: 'ask' } and lets ctx.approval decide; without an approval channel Harness fails closed;
  • allow calls next() and therefore does not override a prior or later policy listener;
  • defaultDecision applies only when no rule matches.

Reasons never interpolate the call arguments. This avoids copying secrets or large payloads into model-visible approval feedback.

Policy decision trace

Set trace: true when operators need to understand why this plugin produced a decision:

trace: true
rules:
  - tool: 'read_*'
    decision: allow
  - tool: 'bash'
    decision: ask
    reason: 'Shell execution requires approval.'

The plugin writes an info record through the Cordis logger with the tool name, this plugin's decision, the one-based matched rule number (or null when defaultDecision was used), and the configured reason for ask or deny. It never includes parsed tool arguments. Tracing is disabled by default, is best-effort, and does not replace Harness session audit events; a later policy listener or monotonic tool guard may still change whether an allowed call executes.

Recipes

See copyable MCP, filesystem, shell, and unattended-task recipes, including their sandbox and approval assumptions.

Architecture

flowchart LR
  model["Model tool call"] --> logged["tool/call logged"]
  logged --> policy["dsh-tool-policy\ntools/pre-execute"]
  policy -->|deny| blocked["Tool error\nbody skipped"]
  policy -->|ask| approval["ctx.approval\nexisting Harness seam"]
  policy -->|allow| guards["Other pre policies\nand monotonic guards"]
  approval -->|allowed-once| guards
  approval -->|rejected or unavailable| blocked
  guards --> execute["tools/execute\nbody\npost-execute"]
  execute --> result["tools/result\nthen tool/result"]

The plugin uses only inject: ['tools'] and ctx.on('tools/pre-execute', ...). Cordis owns listener disposal and reload behavior. The plugin does not patch ToolRuntime or agent-loop.

Example

The checked-in demo loads @deepseek-ai/dsh-system-prompt, @deepseek-ai/dsh-tools, this plugin, and a fixture through the real Cordis Loader. It denies delete_record and allows read_record; the fixture proves the denied body is never invoked.

pnpm build
pnpm integration

Expected output contains:

{
  "blocked": { "isError": true, "message": "deleting records is disabled in the demo" },
  "allowed": { "isError": false, "value": "record:42" },
  "executed": 1
}

Compatibility with DeepSeek Harness

The plugin declares the dsh-tools peer range >=0.1.0-rc.5 <0.2.0 || 0.1.1-rc.2 and Cordis >=4.0.1 <5. It is currently validated against the published 0.1.1-rc.2 registry packages and upstream tag dsh-v0.1.1-rc.2 at commit b150a551b8d465e31e418e1b2eaf5e79bbb7d28e. Its Harness-specific code uses the documented Context, tools service, and tools/pre-execute event only. Peer ranges cannot establish compatibility or detect API drift within those ranges.

The package's dsh.bundle.patch metadata follows the Harness profile-bundle specification. cordis.patch.yml inserts the plugin by its module/package name, and profile composition applies that layer before the profile's own patch. The bundle defaults to deny with an empty rule list; configure the inserted tool-policy row in the profile layer before running tools. DeepSeek Harness exposes MCP tools as mcp__<serverName>__<rawName>, so an mcp__* rule covers the complete MCP namespace.

Current limitations

  • Rules are deployment-global; use multiple Cordis contexts if different agents require different policy trees.
  • The recorded Harness validation baseline is the prerelease 0.1.1-rc.2 registry dependency set and upstream tag dsh-v0.1.1-rc.2. CI tests one locked set, not every version in the peer range; the explicit 0.1.1-rc.2 alternative does not include other 0.1.1 prereleases.
  • Matching supports one condition per rule, JSON Pointer scalar equality, or string containment. It does not implement a general expression language.
  • ask depends on the Harness approval service and an answerer. The plugin does not provide a UI or automatically approve a request.
  • Policy feedback is intentionally argument-free; an operator must inspect the original tool call in the Harness session or telemetry stream.
  • The plugin is a per-call pre-dispatch policy, not capability enforcement. Use the Harness sandbox for filesystem, network, and process confinement.

Roadmap

Implemented

  • Opt-in, argument-free policy decision tracing through the Cordis logger

Next

  • Add reusable routing-oriented policy starters
  • Add copyable MCP, filesystem, and unattended-agent recipes

Future

  • Validate compatibility against the first stable Harness 0.1 release and publish a matching package version
  • Consider a separate, independently scoped rate-limit plugin if deployments need time-window quotas

Development

pnpm install
pnpm typecheck
pnpm lint
pnpm format:check
pnpm test
pnpm build
pnpm integration

The pure matcher is covered by unit tests, the Cordis plugin is exercised through ToolRuntime, and tests/loader.integration.spec.ts boots the actual Harness Loader composition.

License

MIT