Skip to content

This is the multi-page printable view of this section. .

Return to the regular view of this page.

Extend Y8

Start with one task you want to improve. Y8 uses DSH’s plugin system, so you can adopt an existing extension or develop your own without inventing another plugin format.

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 needStart here
A capability already included in Y8Read default plugins and their prerequisites
A capability someone else maintainsSearch the plugin marketplace and check its README and compatibility
A repeatable procedure, prompt, or output standardWrite a Skill for the workspace; a new runtime plugin is unnecessary for instructions alone
Different options or a composition of existing capabilitiesCheck settings, then plugin configuration and Bundle guidance
An existing external serviceCheck the service’s MCP integration before implementing a new connection
A new operation, service, settings card, or interfaceFollow 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

  1. Develop with Creator to inspect available interfaces and experiment with a narrow requirement.
  2. Follow your first plugin to load editable source. This tutorial requires a prepared DSH source checkout, not only the desktop installer.
  3. Add a tool or configuration, changing one behavior at a time and checking its visible result.
  4. 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

Creator helps inspect the running DSH composition and try a small plugin. Use it when a task needs a new operation or interface; start with extension choices if you only need configuration or a repeatable procedure.

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:

pnpm dsh web --no-open --port 0

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

This tutorial creates a minimal Harness plugin and loads it into the Web UI. Start from a repository checkout that has completed the run-from-source path.

Create a local project

From the repository root, create a scratch project for the tutorial:

mkdir -p scratch-plugin/src

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:

import type { Context } from '@deepseek-ai/cordis'

export const name = 'my-plugin'

export function apply(ctx: Context) {
  // Register capabilities here.
}

That is the complete configuration.

Create the plugin file

Create scratch-plugin/src/my-plugin.ts:

import type { Context } from '@deepseek-ai/cordis'

export const name = 'hello-plugin'

export function apply(ctx: Context) {
  // Required dependencies are ready before apply runs.
  console.log('[hello-plugin] plugin loaded!')
}

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:

- insert:
    - id: hello
      name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'

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:

pnpm dsh web --patch ./scratch-plugin/cordis.yml --no-open --port 0

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:

import type { Context } from '@deepseek-ai/cordis'

export function apply(ctx: Context) {
  ctx.effect(() => {
    const timer = setInterval(() => {
      console.log('heartbeat')
    }, 5000)

    // The returned function runs when the plugin unloads.
    return () => clearInterval(timer)
  })
}

Declare dependencies

If the plugin consumes another service such as tools or llm, declare it in inject:

import type { Context } from '@deepseek-ai/cordis'

export const name = 'my-tool-plugin'
export const inject = ['tools']

export function apply(ctx: Context) {
  // ctx.tools is ready here.
  ctx.tools.register(/* ... */)
}

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

import type { Context } from '@deepseek-ai/cordis'

export default {
  name: 'my-plugin',
  inject: ['tools'],
  apply(ctx: Context) {
    // ...
  },
}

Class form

import { Service, type Context } from '@deepseek-ai/cordis'

export default class MyService extends Service {
  static inject = ['tools']

  constructor(ctx: Context) {
    super(ctx, 'myService')
    // Perform synchronous initialization in the constructor.
  }
}

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

3 - Build a tool

This tutorial adds a 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:

import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'greet-tool'
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'greet',
    description: 'Greet someone by name.',
    parameters: {
      name: { type: 'string', required: true, description: 'The name to greet' },
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args) {
      return `Hello, ${args.name}!`
    },
  }))
}

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:

pnpm dsh web --patch ./scratch-plugin/cordis.yml --no-open --port 0

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

4 - Plugin configuration

Accept configuration supplied through 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:

import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'

export const name = 'my-plugin'

export interface Config {
  greeting: string
  maxRetries: number
  verbose?: boolean
}

export const Config: Schema<Config> = Schema.object({
  greeting: Schema.string().default('Hello'),
  maxRetries: Schema.number().default(3),
  verbose: Schema.boolean().default(false),
})

export function apply(ctx: Context, config: Config) {
  console.log(config.greeting)  // User value or schema default.
}

Add the configuration to the inserted local plugin row in scratch-plugin/cordis.yml:

- insert:
    - id: hello
      name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'
      config:
        greeting: 'Hi there'
        maxRetries: 5

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:

import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'

export const name = 'validated-plugin'

export interface Config {
  apiKey: string
  timeout: number
  mode: 'fast' | 'accurate'
}

export const Config = Schema.object({
  apiKey: Schema.string().required(),
  timeout: Schema.number().default(30000),
  mode: Schema.union(['fast', 'accurate']).default('fast'),
})

export function apply(ctx: Context, config: Config) {
  // config is validated and type-safe.
}

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.

// Wrong: hardcoded timeout.
const TIMEOUT = 30000

// Correct: configurable.
export interface Config {
  timeoutMs: number  // Defaults to 30000.
}

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

5 - Package and install a plugin

The previous tutorials loaded a local plugin through a --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 declares dsh.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:

mkdir -p hello-plugin
hello-plugin/
├── package.json       # declares dsh.bundle
├── cordis.patch.yml   # the layer applied when a profile lists this bundle
└── index.js           # plugin modules the patch rows reference

Create hello-plugin/package.json:

{
  "name": "dsh-hello-plugin",
  "version": "0.1.0",
  "type": "module",
  "main": "index.js",
  "files": ["index.js", "cordis.patch.yml"],
  "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
}

Create hello-plugin/index.js with the plugin entry point:

export const name = 'hello-plugin'

export function apply() {
  console.log('[hello-plugin] plugin loaded!')
}

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:

- insert:
    - id: hello
      name: dsh-hello-plugin

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 the dsh.profile manifest with its ordered bundles list.
  • 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:

dsh plugin --profile demo add ./hello-plugin

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:

{
  "name": "dsh-profile-demo",
  "private": true,
  "dependencies": {
    "dsh-hello-plugin": "link:/path/to/hello-plugin"
  },
  "dsh": {
    "profile": {
      "bundles": [
        "@deepseek-ai/dsh-base",
        "dsh-hello-plugin"
      ]
    }
  }
}

Verify the layer without booting, then boot:

dsh --profile demo --dump-config   # shows a "# == dsh-hello-plugin" layer
dsh --profile demo

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:

  1. Each bundle patch named in the profile’s dsh.profile.bundles list, in list order — @deepseek-ai/dsh-base first, then each installed bundle in the order it was added.
  2. The profile’s own cordis.patch.yml.
  3. The home-level $DSH_HOME/cordis.patch.yml — machine-local preferences shared by every profile.
  4. 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 the dsh-web-app bundle overrides dsh-base rows — but must restate every key the row needs, not just the changed one.
  • Users can override your rows in their profile’s cordis.patch.yml without 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:

- id: hello-startup
  name: 'dsh-hello-plugin/startup'

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:

- id: my-app
  name: '@example/my-app'
  inject: [myAppStartup]
  config:
    port: !!js ctx.myAppStartup.port ?? 8080

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:

dsh plugin --profile demo add github:you/hello-plugin

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 prepare script — 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: its prepare runs a dedicated tsdown config that transpiles src/ without project references or type checking.

  • The user allowlists the build. pnpm ≥10 refuses to run a git dependency’s prepare script until it is explicitly allowed, so the first add fails; dsh points at the fix — copy the exact package key pnpm printed into the profile’s pnpm-workspace.yaml:

    allowBuilds:
      dsh-hello-plugin: true

    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 at pnpm publish time; dsh plugin add your-package then installs prebuilt code.
  • Ship a tarball from pnpm pack; users run dsh plugin add ./hello-plugin-0.1.0.tgz.

Next steps