Platform: DeepSeek Harness
Everything here is grounded in the official repository deepseek-ai/deepseek-harness (default branch master). The pages cited are in docs/.
What a plugin is
A plugin is a TypeScript ESM module exporting a function or object form. The function form is sufficient for this plugin:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'dsh-vk-group'
export const inject = ['tools', 'agents', 'storage']
export function apply(ctx: Context, config: Config) {
// register tools, listeners, guard, prompt section, job
}
name— unique within the profile.inject— the Cordis services that must be ready beforeapplyruns. The framework waits for all of them.apply— the body. Everything registered throughctxis cleaned up on unload.
The tool contract (what every vk_* tool must satisfy)
Source: docs/cookbook/adding-a-tool.md and docs/subsystems/tools.md.
A tool is registered via defineTool({ name, description, parameters, output, execute, ... }).
Arguments are declared as a ParameterSchemaSpec — an implicit open object map. defineTool validates model-generated arguments before execute runs, so inside execute the args match InferArgs.
Execute receives (args, exec) where exec carries signal (required cancellation), agent, token. execute returns one canonical JSON value matching output.schema. Human prose belongs in output.render(args, value), not in the return.
Presentation — presentCall(args) and presentResult(args, result) return a card-tagged render intent. They are pure functions — no I/O, no clock, no session state — because they run on both live streaming and session-log replay. The violation of this rule is the most common cause of a "it works now but breaks on replay" bug.
output.render returns ContentBlock[] the model sees; it is the only place model-facing prose goes.
output.schema is a public programmatic API. In PTC mode the model calls await tools.vk_wall_post({...}) and gets the JSON value, not rendered content. So the schema must return useful handles: postId, ownerId, attachmentString, per-id statuses for batch operations.
Rules that bite
executemust honorexec.signal. Long-running uploads poll it between chunks.output(withexecute) is mandatory. A tool without a declared output type is not a tool.- No
output.render→ generic card. If you want a rich UI card, you also needpresentCall/presentResult, not justrender. - Args are readonly. Do not mutate.
output.presentationMetais for durable card state that survives replay (e.g. a post's URL and text), not for tool execution.- Long work (uploads, batch bans) — gate
run_in_backgroundon producer config and route throughctx.jobs.start, not a rawsetInterval.
Config
Config is a Schemastery schema plus a same-named interface. Put defaults on the schema fields. On a config edit, the framework hot-replaces the plugin; registrations clean up, a new apply runs. Carry-over state (LongPoll ts, audit log) lives in ctx.storage, not in the plugin instance.
Bundle and profile
This plugin is a bundle — an npm package with:
{
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } },
"engines": { "dsh": ">=0.2.0 <0.3.0" }
}
cordis.patch.yml is one array entry inserting the plugin row:
- insert:
- id: vk-group
name: dsh-vk-group
Load order (later layers win per row):
@deepseek-ai/dsh-base(first)- this bundle's
cordis.patch.yml - the profile's own
cordis.patch.yml - the home-level
cordis.patch.yml --patch <path>overlays
Users override our defaults in their profile patch without touching our package.
Installation catches
dsh plugin add github:hermes/dsh-vk-groupships sources, notdist/. Apreparescript is mandatory so pnpm can build; the user must allowlist the build inpnpm-workspace.yaml. The wiki's Publishing section documents this.- A tarball (
npm pack) ships prebuiltdist/and needs no build permission. This is the recommended path until the plugin is published to npm.
Hot-reload implications
A config edit unloads and reloads the plugin. The old LongPoll job is aborted and awaited; a new one starts. The ts read from ctx.storage makes the new session resume, not restart from scratch. Any in-flight request that the old plugin started is cancelled by exec.signal and lands as an error result — do not store unconfirmed write results across a reload.
References
docs/capability-seams.md— the full map of extension points.docs/subsystems/tools.md— everyToolDefinitionfield, the schema DSL, the execution pipeline.docs/cookbook/adding-a-tool.md— the authoring reference with card kinds and hard rules.docs/cookbook/extension-cookbook.md— plugin shapes (tool, hook, protocol driver, UI).docs/cordis-primer.md— Cordis dispatch modes.docs/user/develop/basic/index.md— the first-plugin tutorial.docs/user/develop/basic/tool.md— the first-tool tutorial.docs/user/develop/basic/config.md— config + schema.docs/user/develop/basic/publish.md— packaging and installation.