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
- Services dependencies and lifecycle
- Choosing a service event or effect
- A tool policy plugin
- Configuration overlays and rollout
- Testing plugin contracts
- FAQ
- Related resources
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.
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:
- insert:
- id: workspace-notice
name: "/absolute/path/to/deepseek-harness/scratch-plugin/src/workspace-notice.ts"
Start the Web profile with the overlay:
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.
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():
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.
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.
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:
- Pin the DSH revision and every external plugin dependency.
- Keep a reviewed patch file in source control; avoid hand-edited production state.
- Dump the effective plugin tree in staging and compare it with the approved configuration.
- Run capability, denial, cancellation, and unload tests in an isolated workspace.
- Roll out with a narrow tool allowlist and scoped credentials.
- 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.