TL;DR

DeepSeek Harness plugins are Cordis modules that add capabilities through an apply(ctx) function, declared dependencies, services, events, and reversible effects. A correct plugin does more than compile: it uses the narrowest documented seam, disposes its resources on unload, preserves authorization boundaries, and is tested against a pinned DSH revision. This article focuses on those contracts rather than an unstable copy-paste plugin catalog.

Table of Contents

The smallest useful plugin

A DeepSeek Harness plugin is a TypeScript module mounted by Cordis. The official “first plugin” tutorial uses an exported name and apply function. apply receives the runtime context, where a plugin registers behavior through documented services or events.

ts
import type { Context } from '@deepseek-ai/cordis'

export const name = 'workspace-notice'

export function apply(ctx: Context) {
  console.info('[workspace-notice] mounted')
}

This is intentionally small. It proves only that the loader can import and mount the module. It does not provide a tool, a service, authorization, persistence, or cleanup policy. Add one capability at a time and bind each addition to an observable test.

The official tutorial loads a local plugin through an absolute-path cordis.yml overlay:

yaml
- insert:
    - id: workspace-notice
      name: "/absolute/path/to/deepseek-harness/scratch-plugin/src/workspace-notice.ts"

Start the Web profile with the overlay:

sh
pnpm dsh web --patch ./scratch-plugin/cordis.yml

Use this workflow only in a throwaway workspace first. A patch can alter the active plugin tree, so a production patch deserves the same review and rollback discipline as executable deployment configuration.

Services dependencies and lifecycle

Cordis services provide named capabilities on the context. A plugin that needs a service declares it in inject; the framework waits for that dependency instead of relying on implicit registration order.

ts
import type { Context } from '@deepseek-ai/cordis'

export const name = 'guarded-tool-extension'
export const inject = ['tools']

export function apply(ctx: Context) {
  // ctx.tools is available because the declared dependency is ready.
  // Register one narrowly scoped capability here.
}

The pattern has three important boundaries:

Concern Correct contract Unsafe shortcut
Dependency Declare inject for consumed services Reach into an optional service and assume it exists
Lifecycle Register through ctx.on() or ctx.effect() Keep unmanaged timers, sockets, or listeners
Capability Depend on an interface such as ctx.tools Import and mutate a concrete internal implementation

Cordis registrations are reversible effects. Event listeners and supported registrations are removed when the plugin unloads. A resource that needs custom teardown, such as a timer or a network client, should return a disposer from ctx.effect():

ts
import type { Context } from '@deepseek-ai/cordis'

export function apply(ctx: Context) {
  ctx.effect(() => {
    const timer = setInterval(() => {
      console.info('plugin heartbeat')
    }, 30_000)

    return () => clearInterval(timer)
  })
}

Never use a timer as a substitute for durable scheduling or recovery. If the process restarts, the interval disappears. Scheduled work needs a persisted job contract and an explicit recovery model.

Choosing a service event or effect

The DSH architecture distinguishes direct capabilities from lifecycle seams. Use a service method when your plugin calls a capability directly. Use an event when it needs to observe, transform, or govern work moving through the runtime.

flowchart TD A["Need new behavior"] --> B{"Direct capability call?"} B -->|Yes| C["Consume a documented service"] B -->|No| D{"Lifecycle policy or observation?"} D -->|Yes| E["Use a documented event seam"] D -->|No| F["Define a narrow service interface"] C --> G["Register reversible effect"] E --> G F --> G

The official extension cookbook gives useful examples:

  • tools/pre-execute: make an allow, deny, or approval decision before dispatch.
  • tools/execute: wrap the dispatch lifetime for deadline, retry, or metrics behavior.
  • tools/post-execute: transform a result when transformation is required.
  • tools/result: observe the final immutable result for audit or metrics.
  • session/event: project logged events into UI, telemetry, or an external protocol.

These are not interchangeable. Logging a later result cannot prevent a prohibited action, and a permission decision should not be hidden inside a metrics listener.

A tool policy plugin

The following sketch follows the official tools/pre-execute pattern. It demonstrates a narrow policy decision, not a complete authorization system.

ts
import type { Context } from '@deepseek-ai/cordis'
import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools'

export const name = 'repository-write-policy'
export const inject = ['tools']

async function canWriteWorkspace(exec: ToolExecution): Promise<boolean> {
  // Resolve user identity, tenant, workspace, branch, and approval state
  // from application-owned policy services. Do not trust model arguments.
  return exec.tool.name !== 'write_file'
}

export function apply(ctx: Context) {
  ctx.on(
    'tools/pre-execute',
    async (exec, next): Promise<PreToolDecision> => {
      if (!(await canWriteWorkspace(exec))) {
        return { kind: 'deny', reason: 'Workspace write is not approved.' }
      }
      return next()
    },
  )
}

The critical detail is next(). A waterfall listener that only adds a check must delegate to the remaining chain. A listener that deliberately owns the decision can stop the chain with a typed denial. The exact event mode is part of the public contract, so do not assume an event behaves like generic middleware.

This plugin still does not prove that writes are safe. A production policy must bind an authenticated user and tenant to the exact resource, parameters, time window, policy revision, and approval artifact. For the wider problem, see Human-in-the-Loop and Approval Gate.

Configuration overlays and rollout

Profiles, bundles, and patches make a DSH deployment composable, but they also make configuration a behavior-changing artifact. Use an explicit rollout contract:

  1. Pin the DSH revision and every external plugin dependency.
  2. Keep a reviewed patch file in source control; avoid hand-edited production state.
  3. Dump the effective plugin tree in staging and compare it with the approved configuration.
  4. Run capability, denial, cancellation, and unload tests in an isolated workspace.
  5. Roll out with a narrow tool allowlist and scoped credentials.
  6. Keep a rollback patch or previous profile ready before enabling writes.

Do not promise compatibility based on a plugin's name. DeepSeek Harness is in developer preview, and both configuration shape and extension contracts can change. A repeatable test suite is more durable than a tutorial snapshot.

Testing plugin contracts

Test the plugin's observable contract rather than only its happy path. The following matrix is a useful minimum:

Test Assertion
Mount Declared service dependencies are available before apply() runs
Unload Listeners, timers, and clients are disposed
Allowed action A permitted tool reaches execution and produces a result
Denied action A prohibited tool does not reach execution
Approval expiry A stale approval cannot authorize changed parameters
Cancellation Cancellation does not leave unmanaged work running
Upgrade The same suite passes on the intended pinned DSH revision

For a tool plugin, add an integration test with a fake or sandboxed downstream system. Do not validate a write policy by pointing it at a shared repository or real customer data. The Agent Harness evaluation guide covers fault injection and release gates that complement framework tests.

FAQ

Should every DeepSeek Harness feature become a plugin?

No. A capability should become a plugin when it has an independent lifecycle, configuration surface, dependency boundary, or replacement need. Splitting every small helper into a plugin can obscure control flow and complicate compatibility testing.

Can I use private APIs from the DSH repository?

You can technically import source internals from a checkout, but you should not treat them as stable extension contracts. Prefer the documented service and event seams, pin the source revision, and add an upgrade test whenever an internal dependency is unavoidable.

How should a plugin store state?

Choose state by its required semantics. Runtime-only caches can be in memory. Model-visible or replay-relevant facts need durable session events. External side effects need their own application-owned operation and reconciliation record.

Can a plugin add an MCP server?

The DSH extension map describes MCP as a plugin per server that discovers tools and registers them with ctx.tools. Discovery does not authorize tool use: validate the server, tool metadata, tenant scope, arguments, result limits, and downstream effects.

What should be versioned with a plugin?

Version the plugin package or commit, DSH revision, configuration patch, service assumptions, model route, policy revision, test suite revision, and any external protocol or tool schema that changes its behavior.