This is the multi-page printable view of this section. .
Extend Y8
- 1: Develop with Creator
- 2: Your first plugin
- 3: Build a tool
- 4: Plugin configuration
- 5: Package and install a plugin
For a specialized plugin, start with two questions: what data should the domain analysis engine process and what results should it return? What actions and results should the control panel expose? Extend either or both as needed, reusing the existing DSH runtime. See the product philosophy for this division of responsibilities.
Choose an approach
| Your need | Start here |
|---|---|
| A capability already included in Y8 | Read default plugins and their prerequisites |
| A capability someone else maintains | Search the plugin marketplace and check its README and compatibility |
| A repeatable procedure, prompt, or output standard | Write a Skill for the workspace; a new runtime plugin is unnecessary for instructions alone |
| Different options or a composition of existing capabilities | Check settings, then plugin configuration and Bundle guidance |
| An existing external service | Check the service’s MCP integration before implementing a new connection |
| A new operation, service, settings card, or interface | Follow the plugin development path below |
Define a small requirement
Copy this outline into your conversation or project README. Answer what you know; the assistant can inspect the existing project for the rest. Give it sample data rather than credentials.
- Task:
- Inputs and their source:
- Expected output and where I will inspect it:
- Allowed changes and operations that must not run:
- Required accounts, services, and configuration:
- One sample input and its expected result:
For example: read failed jobs from the team’s build service, return failure reasons and links, and never trigger reruns. Use a redacted service response as the acceptance example. This describes a possible extension, not a service already integrated in Y8.
Develop and verify
- Develop with Creator to inspect available interfaces and experiment with a narrow requirement.
- Follow your first plugin to load editable source. This tutorial requires a prepared DSH source checkout, not only the desktop installer.
- Add a tool or configuration, changing one behavior at a time and checking its visible result.
- Follow packaging and installation to make the source reusable. A temporary dynamic experiment is not an installed package.
For a specialized example, read how Harbor is developed. Framework, events, and service details live in the framework guide; that deeper reference opens on GitHub where it is not published on this site.
Share within a team
Share the project or exact package version, supported DSH version, installation steps, configuration names, and an acceptance example. A teammate should supply their own account and directory, then repeat the example. Keep private sessions and credentials out of the shared files. A local checkout link works only on its author’s machine; use the packaging guide when handing it to another person.
If a documented step fails, report the step, version, and error through troubleshooting. This helps distinguish missing guidance from a development capability that needs work.
Contribute to Y8
Desktop builds and site maintenance are separate contributor tasks. Their GitHub references are the desktop source guide, website maintenance guide, and product notices.
1 - Develop with Creator
Prepare the environment
This guide uses the DSH source Web application. Complete the source prerequisites on GitHub, then run the following from the repository root:
Open the launch URL printed by the command, including its authentication fragment. The OS chooses an available port. Select a working directory and configure a model before sending a request. In the session’s Agent Preset selector, choose Creator (创造模式, preset ID cordis). If your installed Y8 version does not expose this preset, use this source workflow; installing Y8 alone does not establish a standalone plugin development environment.
Describe the change
Use the requirement outline and ask the assistant to load the Creator preset’s plugin-development Skill. For composition changes, it also uses the preset’s composition-editing Skill.
For a first experiment, ask for one temporary, read-only tool that returns a short checklist for a task, and ask the assistant to inspect the available tool-registration interface first. State the expected result and require it to stop the experiment after verification. Model requests use your configured account.
Inspect before implementing
Creator can list inspection providers, query their actual interfaces, and inspect the source and diagnostics of an existing experiment. Ask it to select Host for operations and services, Client for UI, or both only when needed. Missing capabilities must be reported rather than replaced with guessed APIs.
Ask for a code preview and the expected effect before activation. Dynamic code uses plain JavaScript function bodies, not TypeScript or JSX modules. Defining an experiment does not execute it. Follow any approval request and inspect the final running state; waiting or starting is not success.
Check and stop
Call the new tool or interact with the changed UI, then compare the result with your example. When something fails, ask Creator to inspect that experiment’s diagnostics and source. Stop it when finished and confirm that the tool or UI contribution disappears. Stopping removes registrations; it does not undo files or external operations already performed.
Keep useful work
Dynamic definitions are process-local and disappear on Host restart. A conversation recording of their source does not automatically reload them. Before restarting, save the useful source and requirements in your project, then use the first-plugin tutorial and packaging guide to build and verify a normal plugin. There is no automatic dynamic-code-to-package conversion in this workflow.
For an existing plugin, edit its original project and preserve its identity and configuration. User presets belong in a user-owned copy; do not edit the shipped Creator preset, which application upgrades replace. Exact runtime rules and diagnostics are documented in the dynamic tool reference on GitHub.
2 - Your first plugin
Create a local project
From the repository root, create a scratch project for the tutorial:
What is a plugin?
In Harness, a plugin is a TypeScript module that exports an apply function. The framework calls apply when loading the plugin and passes a ctx context object through which the plugin registers capabilities:
That is the complete configuration.
Create the plugin file
Create scratch-plugin/src/my-plugin.ts:
Register it in cordis.yml
Run pwd from the repository root, then create scratch-plugin/cordis.yml as a Web overlay that inserts the local plugin. Replace /absolute/path/to/deepseek-harness below with the printed path:
The plugin path must be absolute. A patch file contributes configuration but does not change the profile directory from which the loader resolves module paths.
Start the Web UI with that overlay:
Open the launch URL printed in the terminal, including its authentication fragment. The terminal prints [hello-plugin] plugin loaded! during startup.
Edit and stop
Change the greeting in the plugin, stop the development command with Ctrl+C, and run the same command again. Verify the new greeting in the terminal. Use the newly printed launch URL after each restart; port 0 requests an available port from the OS. Stop the command when finished.
Automatic cleanup
Anything registered through ctx—event listeners, tools, or timers—is cleaned up when the plugin unloads. You do not need to call removeListener or clearInterval manually.
For a resource that needs explicit cleanup, such as a network connection, use ctx.effect() to provide its disposer:
Declare dependencies
If the plugin consumes another service such as tools or llm, declare it in inject:
The framework waits for every required service before loading the plugin.
Three plugin forms
In addition to a function module, a plugin can use object or class form.
Object form
Class form
Function form is sufficient in most cases. Use class form when the plugin provides a service to other plugins; see services and dependencies.
Next steps
- Build a tool — learn the tool definition DSL
- Plugin configuration — accept user configuration
- Cordis tutorial — the plugin framework underneath, built from a scratch directory with no API key
3 - Build a tool
greet tool to the Web UI. Complete Your first plugin first and keep its scratch-plugin directory.Create the tool plugin
Replace scratch-plugin/src/my-plugin.ts with:
inject makes Cordis wait for the tool registry. defineTool infers and validates args from parameters; execute returns the canonical value declared by output.schema, and output.render converts that value to model-facing content.
Run and call the tool
Restart the development command if it is not running:
Open the launch URL printed in the terminal, including its authentication fragment, and ask: Use the greet tool to greet Ada. The model can call greet and receives Hello, Ada! as the tool result.
Next steps
- Plugin configuration — make the greeting configurable.
- Tool authoring reference — look up nested schemas, canonical values, background work, policy hooks, PTC mode, and UI cards.
- Capability layering — split a replaceable capability into Service Definition, Service Provider, and Consumer packages.
4 - Plugin configuration
cordis.yml.Complete your first plugin first. Replace the absolute path below with your checkout path, as in that tutorial; a patch does not change the directory used to resolve plugin modules.
Define the Config type
Export a Config type and a same-named Schemastery schema. Put defaults directly on the schema fields:
Add the configuration to the inserted local plugin row in scratch-plugin/cordis.yml:
When loading the plugin, Cordis uses the exported schema to validate configuration and fill defaults. Do not export a plain object as Config; it does not implement the Standard Schema interface required by Cordis.
Schema validation
Use Schemastery to express stricter validation:
The schema runs while the plugin loads. Invalid configuration fails the load with an actionable error.
Design principles
Do not hardcode tunable values
Harness requires anything that two deployments may want to set differently to be a configuration field.
The test is whether cordis.yml can change the value without a code edit.
Fail loudly on invalid configuration
Express self-contained constraints in the schema so invalid configuration fails while the plugin loads. References to services or registered resources require dependency injection; the services tutorial introduces that contract.
Apply and verify
Stop the tutorial command and restart it after changing the source or patch. With the example above, the terminal prints Hi there. Replace maxRetries: 5 with maxRetries: wrong-type, restart, and inspect the plugin-load validation error. Restore the valid value before continuing. This workflow does not rely on automatic source or overlay reloading.
Next steps
- Package and install a plugin — ship the plugin as an installable package
- Plugins and lifecycle — understand the full plugin lifecycle
- Services and dependencies — provide a service to other plugins
5 - Package and install a plugin
--patch overlay. This tutorial packages it as an installable bundle, installs it into a profile with dsh plugin add, and explains the layer order that determines the composed configuration. It assumes the dsh CLI is installed. Complete plugin configuration first.To use a fresh source checkout instead, complete the run-from-source section, keep this tutorial’s hello-plugin directory at the repository root, and run the remaining dsh ... commands from there as pnpm dsh .... See source execution for build and launcher behavior.
Two concepts, two manifests
Installation is built on two concepts. Both are described by a package.json, but they carry different kinds of manifest under the dsh key, and they answer different questions:
- A bundle is an npm package that ships a configuration layer. Its manifest declares
dsh.bundle, answering “what does this package contribute?”: a patch file that inserts or overrides plugin rows. - A profile is a directory under
$DSH_HOME/profiles/<name>describing one runnable composition. Its manifest declaresdsh.profile, answering “which bundles compose this setup, in what order?”.
A bundle is what you author and distribute; a profile is what a user boots with dsh --profile <name>. Nothing is both.
The bundle manifest
Create the package directory:
Create hello-plugin/package.json:
Create hello-plugin/index.js with the plugin entry point:
Create hello-plugin/cordis.patch.yml. The patch is a YAML array like the --patch overlays you wrote, except plugin rows reference the package by name instead of a relative source path so Node resolution finds the installed code:
A package without the dsh.bundle declaration still installs, but only as a plain dependency: dsh plugin prints a warning and activates no layer. Use that package format for a library that plugin packages import rather than a plugin users enable.
The profile manifest
A profile directory holds two files:
package.json— the profile’s out-of-tree plugin dependencies (managed by pnpm) plus thedsh.profilemanifest with its orderedbundleslist.cordis.patch.yml— the user’s own patch layer, applied after every bundle layer.
You never write a profile manifest by hand: dsh --profile <name> --from-default-profile <template> can create one from a shipped application template, while dsh plugin creates a base-backed profile and maintains its installed bundle list. The CLI behavior reference owns the creation rules; the next section shows the plugin path.
Install into a profile
dsh plugin --profile <name> <args...> forwards to pnpm in the profile directory, so every pnpm verb works. From the directory that contains hello-plugin, install the package checkout:
The first use initializes the profile (with @deepseek-ai/dsh-base as its first bundle), pnpm links the checkout, and dsh appends the bundle to dsh.profile.bundles because the package declares dsh.bundle:
Verify the layer without booting, then boot:
dsh plugin --profile demo remove dsh-hello-plugin removes both the dependency and the layer.
The loading order
The effective configuration composes over an empty root by applying, in order:
- Each bundle patch named in the profile’s
dsh.profile.bundleslist, in list order —@deepseek-ai/dsh-basefirst, then each installed bundle in the order it was added. - The profile’s own
cordis.patch.yml. - The home-level
$DSH_HOME/cordis.patch.yml— machine-local preferences shared by every profile. - Each
--patch <path>overlay, in argv order.
App arguments are not another patch layer. A surface bundle can resolve them through an ordinary app-owned service, described below.
Later layers win per row, and a patch replaces a row’s entire config value rather than deep-merging keys. Two consequences for bundle authors:
- Your patch can override rows from earlier layers by
id— the same way thedsh-web-appbundle overridesdsh-baserows — but must restate every key the row needs, not just the changed one. - Users can override your rows in their profile’s
cordis.patch.ymlwithout touching your package, so prefer configuration defaults users are likely to keep and let the schema carry the rest.
In-box bundle names always resolve from the dsh installation itself; pnpm manages only out-of-tree packages, so your bundle can rely on @deepseek-ai/dsh-base being present and current.
Give a surface bundle its own command line
A bundle that defines a runnable app mounts an ordinary provider plugin:
The plugin exports inject = ['cmdlineArgs'], calls parseCmdline from @deepseek-ai/dsh-cmdline with its own commander program, and provides its app-owned service from the program’s action. The launcher hands every plugin the same immutable arguments after launcher flags, so app-specific flags need no launcher change and multiple plugins may parse the snapshot. The Loader row needs no launcher marker or special kind.
Rows configured by those arguments inject the provider’s service and read it from their own !!js options, with the deployment value beside it as the fallback:
On --help, the provider publishes no service, so those rows never activate. Loader mounts the composition once, waits for each row’s ordinary injections, and only then evaluates that row’s !!js config against its injected context.
Installing from GitHub: the build-script catch
Publishing to a registry is not required — users can install straight from a git host:
But a git install fetches sources, not built artifacts: nothing runs your build script, so a TypeScript package arrives without its lib/ output and fails to load. Two things must happen, one on each side:
The author ships a
preparescript — pnpm runs it after a git install — that builds the published entry points from source, self-contained: it must not assume dev-only context such as a sibling monorepo checkout. turtle-ui is a working example: itsprepareruns a dedicated tsdown config that transpilessrc/without project references or type checking.The user allowlists the build. pnpm ≥10 refuses to run a git dependency’s
preparescript until it is explicitly allowed, so the firstaddfails;dshpoints at the fix — copy the exact package key pnpm printed into the profile’spnpm-workspace.yaml:and re-run the
add.
Treat that allowance as permission to execute the package’s code on your machine at install time, outside any sandbox the agent runs under. Only allow packages whose source you trust, and pin a commit (github:you/hello-plugin#<sha>) so a later push cannot silently change what runs.
If you would rather not ask users for the allowance, distribute built artifacts instead — neither form needs any build permission:
- Publish to npm with
lib/built atpnpm publishtime;dsh plugin add your-packagethen installs prebuilt code. - Ship a tarball from
pnpm pack; users rundsh plugin add ./hello-plugin-0.1.0.tgz.
Next steps
- Plugins and lifecycle — the full plugin lifecycle
- CLI behavior reference — exact layer precedence, flags, and profile mechanics