DSH plugin isolation: DeepSeek Harness inject and isolate
A DSH plugin "service" is a named capability a plugin mounts on ctx and exposes to other plugins, inject declares which services it needs, and isolate in cordis.yml gives different plugin groups their own instance (source). This article covers how to write required versus optional dependencies, the runtime behavior when a required service disappears and returns, and isolate multi-instance isolation plus type declaration merging; a Cordis primer is in the Cordis primer, and the writing split between inject and ctx.get is in the plugin spec, while this article covers dependency runtime behavior and isolation.
Required and optional dependencies in a DSH plugin
In Harness, tools, llm, and agents are services — named capabilities mounted on ctx (ctx.tools / ctx.llm / ctx.agents) — and any plugin can provide a service for other plugins to consume (source). There are two ways to consume a service, and the difference is when the plugin loads:
- Required dependency — export
const inject = ['tools']. The framework guarantees every service declared byinjectis ready whenapplyruns; if a service is not ready, the plugin waits instead of running. - Optional dependency — omit
injectand query withctx.get()at the use site, with optional chaining as a fallback:
// Required: the plugin does not load while the service is absent.
export const inject = ['tools']
// Optional: omit inject and query with ctx.get() at the use site.
export function apply(ctx: Context) {
const metrics = ctx.get('metrics')
metrics?.record('plugin_loaded', 1)
}
To provide a service, use the Service base class from @deepseek-ai/cordis: extend it, call super(ctx, 'metrics') in the constructor to set the service name, and optionally declare static inject = ['llm'] since a service may depend on other services; consumers then access it as ctx.metrics. To type it correctly, use TypeScript declaration merging:
declare module '@deepseek-ai/cordis' {
interface Context {
metrics: MetricsService
}
}
DSH plugin runtime behavior when a service disappears and returns
If a required service disappears while the application is running, for example because its provider unloads, dependent plugins dispose automatically to release resources, and they load again when the service returns (source). The point of this behavior is to prevent a plugin from calling a service that no longer exists.
In other words, declaring a service as a required dependency is not only a startup-time "wait until it is ready" constraint but also a runtime availability contract — changes in service availability propagate automatically to the loading state of dependents. That is why the choice between required and optional depends on whether your plugin can tolerate absence: if it can, use ctx.get() for an optional dependency; if it cannot, let inject have the framework manage the lifecycle.
DSH plugin cordis.yml isolate multi-instance isolation
cordis.yml can isolate services so separate plugin groups see separate instances of the same service (source). The typical pattern gives two group: true plugin groups their own isolated copy of a same-named service:
- id: group-a
name: '@deepseek-ai/cordis-plugin-group'
group: true
isolate:
shell: true
config:
- name: '@deepseek-ai/dsh-bash-local'
config:
timeoutMs: 5000
- name: './src/plugin-a.ts'
- id: group-b
name: '@deepseek-ai/cordis-plugin-group'
group: true
isolate:
shell: true
config:
- name: '@deepseek-ai/dsh-bash-local'
config:
timeoutMs: 60000
- name: './src/plugin-b.ts'
plugin-a and plugin-b each see the Bash instance in their own group, with no cross-group effect — group-a's timeoutMs: 5000 and group-b's timeoutMs: 60000 never overwrite each other. This is exactly the landing spot for "one capability interface, multiple isolated implementations," and it pairs with the layering in capability seams.
One last discipline for looking up services: the repository generates built-in Harness service names, public methods, and source locations into each service's subsystem page (such as the core subsystem page). While developing a plugin, use those generated regions and the service's TypeScript interface, and do not maintain a second static list, which would drift from the generated interface.
Three self-checks when done: can this dependency tolerate the service being absent (deciding inject versus ctx.get()); did you add a type declaration merge for the service name; and, under isolation, did you place the same-named service inside the right plugin group's isolate. To find plugins commonly paired with isolation (local shell, model adapters), search the DSH Plugin Hub; for extension-point ownership see the feature-to-mechanism map.
FAQ
In a DSH plugin, declaring export const inject = ['tools'] makes a required dependency: the framework guarantees every service declared by inject is ready when apply runs, and a plugin waits instead of running while a service is not ready. An optional dependency omits inject and queries at the use site with ctx.get('metrics') plus optional chaining, e.g. const metrics = ctx.get('metrics'); metrics?.record('plugin_loaded', 1), so a missing service never blocks the plugin from loading.
When a required service a DSH plugin depends on disappears at runtime, for example because its provider unloads, the dependent plugins dispose automatically to release resources; they load again when the service returns. This behavior prevents a plugin from calling a service that no longer exists, so declaring a required dependency means letting the framework manage your plugin's availability lifecycle.
A DSH plugin provides a service with the Service base class from @deepseek-ai/cordis: extend Service, call super(ctx, 'metrics') in the constructor to set the service name, and optionally declare static inject = ['llm'] to show the service itself depends on other services; consumers then access it as ctx.metrics. Use TypeScript declaration merging with declare module '@deepseek-ai/cordis' to add metrics: MetricsService to Context so ctx.metrics is typed.
A DSH plugin uses isolate in cordis.yml for service isolation: the same service can have multiple instances, and different plugin groups see different ones. The typical pattern gives two group: true cordis-plugin-groups each isolate: shell: true and a copy of @deepseek-ai/dsh-bash-local (with different timeoutMs); plugin-a and plugin-b each see the Bash instance in their own group, with no cross-group effect.
A DSH plugin developer should treat the repository-generated service names, public methods, and source locations as authoritative: they are generated into each service's subsystem page (such as the core subsystem page) and used alongside the service's TypeScript interface. The official docs require not maintaining a second static service list, which would drift from the generated interface.
Related Terms
- Service
- Service is a named capability one plugin exposes to other plugins, mounted on ctx, for example ctx.tools, ctx.llm, and ctx.agents; any plugin can provide a service by extending the Service base class from @deepseek-ai/cordis and calling super(ctx, 'name') in its constructor.— https://deepseek-harness.github.io/deepseek-harness/en/develop/framework/service
- inject
- inject is the exported constant by which a plugin declares the services it needs, e.g. export const inject = ['tools']; the framework guarantees those services are ready when apply runs, and the plugin waits instead of running while any is not ready, which makes inject a required dependency.— https://deepseek-harness.github.io/deepseek-harness/en/develop/framework/service
- ctx.get()
- ctx.get() is how an optional dependency is queried at the use site: omit inject, call ctx.get('metrics') where needed, and handle absence with optional chaining so the plugin loads and runs even without that service.— https://deepseek-harness.github.io/deepseek-harness/en/develop/framework/service
- service isolation (isolate)
- Service isolation is a cordis.yml setting: through a plugin group's isolate field (e.g. isolate: shell: true), the same service holds one instance per plugin group, so plugins in a group only see their own instance with no cross-group effect.— https://deepseek-harness.github.io/deepseek-harness/en/develop/framework/service