DSH Plugin Development
This is an execution checklist oriented towards official releases. First determine the runtime context, then select the official template, and after implementation, verify through real combinations and user installation paths. Do not treat accidental implementations of a certain project as framework contracts.
1. Before Starting
- Confirm the project and user changes using ,
git rev-parse --show-toplevel
, and git status --short --branch
.
- Read , , , build configurations, related files and tests.
- Do not overwrite user changes, and do not manipulate profiles, ports or instances explicitly excluded by users.
- Determine the minimal runtime context:
- Tools, system prompt, HTTP, persistence, provider: host.
- Slots, Conversation Nodes, browser state and floating layers: client.
- Host capabilities that require Web visualization: host + client.
- No Web requirements: Do not declare , and do not build client bundles.
- Write down the plugin's unique responsibilities, dependent services, contributed configuration lines, persistence owners, and user-visible verification surfaces before starting coding.
2. Evidence and Official References
2.1 Evidence Collection Order
When behavior is uncertain, collect evidence in the following order instead of guessing:
- , exports, types, and README of the current project and installed
node_modules/@deepseek-ai/*
.
- DeepSeek Harness checkout provided by the environment; analyze in read-only mode, do not modify.
- Clone the official repository for evidence collection (see §2.3).
- If information is still insufficient, take the current official version's exports/types as the boundary, choose the minimal implementation that can fail safely and mark assumptions.
Do not hardcode local absolute paths, and do not access or relay content from unauthorized private repositories.
2.2 Official Template Selection
If a Harness checkout is provided (by the environment or cloned as per §2.3), prioritize reading these templates according to the plugin type; paths are based on the checkout root directory:
| Objective | Primary Reference | Key Learning Points |
|---|
| Host Service / HTTP | | , , , route disposer, connection cleanup |
| Minimal client plugin | packages/client/ui-message-feedback
| , , locale, per-session controller, slot registration and cleanup |
| Slot / Conversation Node | packages/client/ui-conversation
+ | , slot kind/scope, children claiming, keyed node renderer |
| Bundle Layering | + | Top-level patch array, line ID override, whole-section config replacement, loading order |
| Simple persistence backend | packages/storage/storage-json
| register → disposer → close, explicit root, concurrent access guard |
| Crash-safe logging | packages/session/session-persistence-jsonl
| Atomic publish, fsync, concurrent no-clobber, torn-tail handling |
| Tool plugin | | , schema, render, optional capability mounting |
| Client testing | packages/test-support/client-runtime
| jsdom, SlotTestRuntime, mount/dispose, fake service |
Complex plugins are only used to supplement evidence, not as starting templates. If delegating read-only research, the prompt must require specifying files, line ranges, contracts and minimal suggestions.
2.3 Official Repository Fallback
The official repository
https://github.com/deepseek-ai/deepseek-harness
is a public, MIT-licensed reference source (default branch
; no release tags during developer preview, do not pin versions). For fallback evidence collection:
-
Choose a temporary directory: Use a directory provided by the user or environment, e.g.,
; do not hardcode local absolute paths.
-
Reuse existing checkout: If
exists, and
points to the official repository, and the root directory contains
and
, reuse it directly; to update, run
git -C "$SCRATCH/dsh-official" fetch --depth 1 origin master && git -C "$SCRATCH/dsh-official" reset --hard origin/master
(or delete and re-clone). Maintain only this one directory per task to avoid repeated cloning.
-
Shallow clone (read-only for evidence collection, no need for
):
sh
git clone --depth 1 https://github.com/deepseek-ai/deepseek-harness.git "$SCRATCH/dsh-official"
-
Only clone the official
deepseek-ai/deepseek-harness
; do not access or relay content from unauthorized private repositories. Analyze cloned content in read-only mode, do not modify.
After entering, locate content in the following order:
- First read the root ( is its symlink): It clearly explains repository layout, commands and conventions, and is the official entry point for agents.
- Then use the group table in to confirm which the target package is located in.
- Read the corresponding package's and according to the template table in §2.2; specify files and line ranges in evidence conclusions.
Evolution fallback: The official repository is in developer preview, iterates rapidly, has no compatibility commitments, and no release tags. The template paths in §2.2 are only indexes; everything is subject to the actual code of the current checkout. If paths or names drift, locate the new position using
and report corrections, do not guess based on old documents. Record
when consistent evidence reproduction is needed.
3. Bundle, Profile and Package Contracts
3.1 Two Concepts
- Bundle: The package distributed by the author:
package.json.dsh.bundle.patch
points to the configuration layer.
- Profile: The combination run by the user:
$DSH_HOME/profiles/<name>/package.json.dsh.profile.bundles
stores the ordered bundle list.
- Plugin authors write bundles; creates and maintains profiles. Do not manually write user profile manifests.
3.2 Minimal Dual-Face Package
jsonc
{
"name": "dsh-my-plugin",
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": { "types": "./lib/types/index.d.ts", "default": "./lib/index.js" },
"./client": { "types": "./lib/types/client/index.d.ts", "default": "./lib/client.js" },
"./cordis.patch.yml": "./cordis.patch.yml",
"./package.json": "./package.json"
},
"files": ["lib", "cordis.patch.yml", "README.md"], // Directories or explicit lists are acceptable; official repositories often use explicit file lists
"dsh": {
"bundle": { "patch": "./cordis.patch.yml" },
"client": {
"platform": "web",
"inject": ["@deepseek-ai/dsh-client-runtime"]
}
}
}
Rules:
- Remove and for host-only packages.
- Client packages must have both
dsh.client.platform: "web"
and a valid .
- is informational metadata delivered with the bundle (used for pre-check display / HMR diff), and does not determine the activation order of client fibers; prefetch is driven by , and real dependency waiting comes from exported by the client bundle (§5.1), the two are not interchangeable.
- is an optional prefetch mark only for startup critical entries; do not enable it by default for ordinary third-party plugins.
- The current authoritative field is ; historical compatible fields are only added if the target official deployment explicitly reads them.
- Exports, and Git/release artifacts must be consistent; no entry can point to a non-existent file.
- Shared runtimes such as DSH, Cordis, React are preferably declared as peers to avoid duplicating runtime identity; version ranges are collected from the target official version's package metadata.
3.3 Patch Layer
must be a top-level array:
yaml
- insert:
- id: my-plugin
name: dsh-my-plugin
config: {}
Notes:
- is the stable line identity in the configuration tree; is a Node-resolvable package name or export path.
- Later layers override earlier layers by ; the of the target line is a full-section replacement, not a deep merge, so required keys must be restated when overriding.
- The effective order is profile bundles → profile →
$DSH_HOME/cordis.patch.yml
→ command line ; the latter takes precedence.
- Packages without will only become ordinary dependencies and will not automatically become part of the profile layer.
4. Host-Side Implementation
4.1 Function Plugins
Ordinary plugins usually export:
ts
export const name = 'my-plugin'
export const inject = ['tools']
export interface Config { enabled: boolean }
export const Config = z.object({ enabled: z.boolean().default(true) })
export function apply(ctx: Context, config: Config): void {}
- is imported from (not zod); references the exported schema, which is equivalent to the official inline
static Config: z<Config> = z.object({...})
.
- is the required service; if not satisfied, the fiber remains pending, and the framework will activate it after the service is ready, do not use polling to simulate dependency injection.
- Config default values are placed in the schema; any value that may need to be changed in deployment should be a configuration, not a source code constant.
- Optional services are judged using or lazily mounted using
ctx.inject([...], childCtx => ...)
; do not preempt sibling providers in .
4.2 Service Plugins
When a plugin provides stable services, refer to
:
ts
export class MyService extends Service {
static Config = Config
constructor(ctx: Context, config: Config) {
super(ctx, 'myService')
}
async [Service.init](): Promise<void> {}
}
- The constructor declares the service key; asynchronous startup is placed in .
- Initialization failures should cause the fiber to fail and be reported by the starter, do not swallow combination errors.
- The registration method returns a disposer; the party that owns the resource is responsible for closing it.
4.3 Effect Ownership
All long-lifecycle resources must belong to the current fiber:
- Routes, listeners, watchers, timers, React roots, DOM, sockets, temporary services must all be cleanable.
- Use or
ctx.effect(() => disposer, label)
.
- The disposer order is usually: stop external entry/unregister registry → wait for or cancel in-progress work → close resources.
- When services need to be bound later, use "immediate attempt + service event/ retry + idempotent guard", do not register repeatedly.
4.4 Tools
Use
ctx.tools.register(defineTool(...))
:
- clearly states when to call, necessary prerequisites, failure semantics and side effects.
- Both and use the value-schema DSL from (compiled into a supported subset of JSON Schema): is an implicit open object root, required fields use inline in attributes; declares the canonical return value and is forcibly validated by
assertSupportedJsonSchema
during registration. The two are two aspects of the same DSL, not two sets of languages.
- provides stable, compact, determinable text for the model.
- Obtain the current session, workspace and owner from , do not guess from global process state.
- Observe or forward for asynchronous work; write operations must have idempotent, lock or conflict strategies.
4.5 HTTP
- Inject the current official version's Web server service, and use a structured minimal interface to reduce coupling.
- Routes are registered via
ctx.effect(() => ctx.webServer.register({ kind: 'exact' | 'prefix', path, handler }))
; duplicate (kind, path) will throw an error.
- Explicitly set cache policies for status interfaces: sensitive or real-time snapshots prioritize , re-verifiable resources use ; static resources use explicit whitelists and correct content types.
- Path decode, request body parsing and handler rejection must all be converted to explicit 4xx/5xx, cannot become unhandled rejections.
- Ownership of exact routes, longest prefixes, and fallbacks cannot conflict; unknown plugin resources return 404, do not fall into SPA fallback.
- When involving permissions or local capabilities, adopt minimal exposure, loopback/trust boundaries and method whitelists.
4.6 Persistence and Concurrency
First determine whether to reuse the official version's storage/session persistence service, or if the plugin has an independent medium. In either case:
- Path configurations are explicitly specified; do not use defaults to scatter user data.
- States are isolated by clear dimensions such as workspace, session, owner or business ID.
- Read-modify-write operations for the same resource are serialized; concurrent creation uses no-clobber semantics.
- Human-readable JSON uses temporary files in the same directory + fsync + atomic publish; append logs handle torn tails. Concurrent creation uses the no-clobber protocol of +, do not use to silently overwrite.
- The cleanup order for registry backends is unregister then close.
- Recovery and HMR cannot assume that creation events will be replayed; if needed, explicitly scan and backfill existing objects.
5. Client-Side Implementation
5.1 Minimal Entry
ts
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
export const inject = ['slots']
export function apply(ctx: ClientContext): void {}
- Type contributions use type-only imports to pull in Context/SlotMap merges.
- Client registrations, controllers, listeners, styles and DOM must all be disposed along with the client fiber.
- Per-session states are bucketed by ; only resynchronize objects that have been read when the connection is reset.
5.2 Four-Step Slot Contract
- Declaration: Pull in types from the official package that provides slots; only extend via module augmentation for custom owners.
- Claiming: The parent entry's table declares child slots; declaration means occupying rendering rights, do not compete for others' seats.
- Registration: The activation order of owners and contributors is not guaranteed; use
ctx.slots.inject(key, () => ctx.slots.register({ name, children?, store?, locale?, inject?, ...kind parameters }, Component))
to wait for declaration; is also the claiming table for child slots (claiming means occupying rendering rights). Kind parameters: keyed requires , list requires (can add /), chain requires ; single/keyed/list can add for cell hiding (same cell with same priority will throw an error). Directly registering to an undeclared slot will throw an error.
- Rendering: Owners use /; contributors do not import the owner's implementation components.
When choosing seams, first check the current official version's types. Common session UI seams include:
conversation.session.header.actions
/
,
,
,
conversation.chat.commandview
,
conversation.chat.assistant-actions
,
conversation.chat.turnTail
,
,
conversation.composer.dock
,
conversation.composer.bar
,
/
/
/
. Global floating layers use
(list/root), do not touch the
single slot. Do not write slot names based solely on old documents, refer to the SlotMap in the current official version's
ui-conversation/src/client/contract/slots.ts
.
5.3 Conversation Node
Conversation Node is a combination of "event folding + keyed slot renderer":
- Define shared event types and merge them into the session event map.
conversationEvents.register(definition)
:
- selects events;
- creates node states;
- folds deterministically by seq;
- generates stable view nodes.
- Merge /node kind types.
- Register a renderer with the same key to .
Red Lines:
- Replaying the same event sequence must produce the same node, do not read time, random numbers or current disk state.
- returns a stable business ID and role; the node engine uses
conversationContextKey(kind, businessId)
for deduplication within the current session. Cross-session persistent caching separately includes the owner session in the key, cannot be confused with engine contracts.
- Events are written to the business owner's session; shared host/client event files remain type-only, preferably with zero runtime imports, to avoid Context augmentation conflicts between dual tsconfigs.
- Disk/server snapshots can be used as the source of truth for real-time UI; event streams are used for dialogue projection, auditing and deterministic history, their responsibilities should not be confused.
5.4 Portal Fallback
Use semantically correct slots instead of fixed portals whenever possible. Full-application floating layers prioritize registering to
(list/root, click-through until your entry actively enables pointer events); only use body portals when there is truly no global corner slot:
- React roots, host DOM, window listeners, global attributes all have disposers.
- Follow the session list and filter by current owner; collapse immediately when navigating.
- On wide screens, allow the main column to yield, on narrow screens fall back to overlay; only rely on stable attributes, do not couple with hash classes.
- Existing activities restored on the first screen only display logos, avoid automatic expansion after the first request returns causing large layout shifts; new activities that appear after stabilization can expand automatically.
- Panels are limited to a portion of the container/viewport height, with internal scrolling in the content area; set a separate upper limit for narrow screens.
- Polling uses , in-flight guard, response shape validation and unmount protection; retain the last successful snapshot on failure.
- Support keyboard, , , Escape, reduced motion; hover/focus only previews, click to fix state.
6. TypeScript and Client Building
6.1 Dual tsc Programs
Host and client use two separate programs; file names can be chosen according to project layout. Official repositories use two aggregate programs
and
for host/client checks respectively: host excludes
and
tests; client aggregates CSS module declarations, client tests and build scripts for each client package, shared leaf packages are included via project references, and each
package also maintains its own composite tsconfig for intra-package type checking. JSX uses
and
; relative TS imports must be correctly rewritten to emitted JS.
This avoids declaration merge conflicts for Context services with the same name between host sessions and browser runtimes.
6.2 Client Bundle
Prioritize reusing the current official version's Harness client tsdown helper or verified templates, do not write loader protocols manually. Artifacts should be automatically wrapped by the build as:
js
window.__ModuleLoader__.load({ id, factory: (require) => { /* bundle */ } })
The build must retain:
- Coexistence of host/client artifacts (client build does not clear host output);
- Sourcemaps;
- CSS Modules compilation and injection;
- Path fallback to retrieve resources from emitted ;
- Client bundle purity gate.
6.3 Client Import Purity
The browser module table only answers official version platform seed modules and explicit exemptions. Rules:
- Platform modules are based on the official version's
packages/client/web/src/platform.ts
and official client build configurations; React, Cordis, slots, web-react, primitives, attachment, schema-form are provided by the module table.
@deepseek-ai/dsh-client-runtime/client
is an explicit temporary exemption in the official build configuration, not an ordinary platform module; do not generalize it as permission to arbitrarily import runtime values.
- Pure type imports are erased, can pull in type contributions across packages.
- Wire types, generated remote codecs or explicitly vendored pure libraries can only be inlined if allowed by official templates.
- Other cross-plugin value imports are prohibited; collaboration must go through Cordis service/remote/slot. Otherwise, either the build-time purity gate or runtime require will fail.
7. Distribution, Installation and Effective Boundaries
7.1 Installation
dsh plugin --profile <name> <args...>
is a pnpm forwarding layer in the profile directory, and reconciles the bundle list according to installation status and
after success. Therefore, it supports npm, path, tarball and Git:
sh
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:<owner>/<repo>
GitHub distribution does not require publishing to npm, but one build strategy must be selected (Git retrieves source code, not build artifacts):
- Official Recommendation: Provide self-contained (official turtle-ui mode); pnpm ≥10 blocks build scripts for Git dependencies by default, users need to explicitly add to the profile's and re-run . This executes third-party code, so commit should be fixed and only trusted reviewed repositories should be used.
- Alternative (Non-interactive Installation): Commit the complete, latest pointed to by exports into Git; users do not need to execute dependency scripts, but this is not the official recommended path.
The README only provides verified recommended commands for fresh profile installations. Restart the target profile after installation.
7.2 HMR and Restart
- Client HMR requires a build watcher such as to continuously rewrite ; host HMR only detects file changes via stat, then triggers browser fiber dispose/reload via rev/SSE.
- Only bundle content changes can trigger client HMR; changes to package manifests, exports, plugin collections, profile bundles and host code require a restart.
- When there is no watcher after a normal build, refresh the existing DSH page.
- Do not start an independent Vite server to replace the DSH GUI; the Web shell depends on injected by the host.
8. Verification Matrix
8.1 Baseline
sh
pnpm typecheck
pnpm build
pnpm test # Run if declared in package.json
pnpm verify # Run if declared in package.json
git diff --check
First read
, do not assume all repositories have the same aggregated scripts: Official Harness uses
/
and multiple
gates; third-party plugins can customize
. Project-level verify/check should cover at least:
- Pure business rules and state transitions;
- File round-trips, locks, archiving/restoration in temporary directories;
- Client-side projection/folding pure functions that can be tested independently;
- Canonical Skill and mirror consistency (if the project provides mirrors).
8.2 Host and Real Combinations
- Unit tests cover schemas, services, failures and disposers.
- Use shared contract suites when there are registry/backend interfaces.
- Do not only manually call : At least one test starts via real Loader/patch combination and asserts user-visible surfaces.
- First create a non-built-in scratch profile using
dsh plugin --profile <scratch> add <pkg>
, then execute dsh --profile <scratch> --dump-config
to confirm bundle layers, line IDs, names, configs and injection order; built-in / profiles can be initialized by the launcher. There is also : Only prints bundle layers, skips user layers and , can be used for recovery diagnosis when is broken.
- Use
dsh --profile headless "a small, determinable task"
for real tasks; do not invent the subcommand.
8.3 Client
- Client tests use the jsdom lane; mount plugins via SlotTestRuntime or minimal fake services.
- Assert slot registration, rendering, session isolation, connection reset, and cleanup of registry/DOM/style/controller after dispose.
- At least one HMR/dispose safety test for each registry contribution.
- Use an independent web profile and real browser for GUI verification, including roster, routing, interaction, refresh, wide/narrow screens, scrolling, focus and reduced motion.
8.4 Zero-Install and Git Distribution
- Use a brand-new temporary /profile.
- Install according to the exact commands in the README.
- Assert profile dependencies and .
- Assert that all exports, host/client bundles, patches and static resources exist.
- The plugin layer must appear in .
- After startup, check host routes, client roster and real UI.
When the repository is still private, copy the content to be released to a temporary Git repo and commit, then install via
; this verifies "content retrieved via Git" instead of uncommitted files in the current checkout. Prerequisites:
is in PATH, the directory is a committed real Git repository; if the package declares
,
must also be added to the profile's
(same gate as §7.1). Only delete the exact temporary directory created for this task.
9. Completion Criteria
Confirm the following items one by one before completion:
- Minimal runtime context, consistent manifest, exports, patches and artifacts.
- Clear boundaries between required injects and optional services; pending/failed states are diagnosable.
- Routes, registries, timers, watchers, DOM, React roots and storage are all cleanable.
- Conversation Nodes can be deterministically replayed, with correct owners and deduplication dimensions.
- Client imports do not cross the module table, host/client types are isolated.
- Persistence has concurrency and crash semantics, does not rely on accidental cwd.
- Typecheck, build, verify, real combination, zero-install and required GUI verification pass.
- README installation commands are consistent with the actual distribution form.
- No unauthorized commits, pushes, releases or visibility changes have been executed.