DSH plugin development
General reference for writing dsh plugins. It records durable conventions and gotchas — the what / why, not the where — so re-locate the current files by grepping the harness rather than relying on memorized paths.
Locating the harness
- If the user or the current session has named a local checkout, first check that its branch is aligned with upstream; if not, fetch and pull the latest before relying on that checkout. Then read it.
- Otherwise, analyze the published package — the packages under (what runs) — rather than assuming a local source tree exists.
Profile
dsh's user config lives under
(default
). A
profile is a named composition under
, holding:
- — out-of-tree plugin plus the manifest with its ordered list. maintains it; never write it by hand.
- — the user's own patch layer (id-targeted whole- overrides, disables, and lists). It is an override layer, not the mechanism a plugin loads through.
- — plugins pnpm installed for this profile.
Layers apply over an empty entry list in order: each bundle patch in
order, then the profile's
, then the home-level
$DSH_HOME/cordis.patch.yml
, then each
overlay. Pick a profile with
(the GUI
profile is the common one). A profile is unrelated to a workspace/cwd.
Preset
A preset is an agent-plane composition — the tools, prompt sections, and services that build one session's agent. It is orthogonal to a profile: a profile is process/host-plane (registries, sandbox, approval, persistence, model route), while a preset decides what each agent can do.
- Each preset is one agent-plane roster (named presets include , , , , ), mounted once per process under a standing scope; every session naming it joins by scope parentage.
- A preset mounts its own per-agent copies (shell/filesystem tools, skills discovery + catalog loader, goals, plan mode, compaction, delegation/workflows); the host composition keeps the shared registries.
- Per-agent service rows sit inside a group carrying an realm (entry-local = one private instance per mounted session).
- Use it by creating a session against a preset —
session.create({ agentPreset })
; the id is stored on the session header and resume rebuilds the same agent. The default is the user's stored choice, else the deployment default ().
- Manage presets through the RPCs (list / select / read / copy / remove / openDocument).
Package shape
- One plugin = one npm package, named under a user scope: when no scope is explicitly specified, default to the current user's git config —
@<git-user.name>/dsh-plugin-<name>
. Never use the scope for a third-party plugin — it is reserved for the harness's own packages. Depending on via is expected; the reserved-scope rule is about the plugin's own package name.
- carries / (, ), a block — pointing at the package's own , plus optional (, , ) — a list that ships every built entry and , and against .
- The package's inserts its own host row by package name (never a relative source path), so Node resolution finds the installed code.
- Layout: (host entry), (client entry), (pure logic with no dsh imports → unit-testable with ), (i18n dictionaries), .
- Host entry: default-export a Cordis Service — , , init in — or a plain .
- Client entry: → , exporting (service names) + ; render with (no JSX).
Constraints
- Naming. Third-party plugin packages are scoped: when no scope is explicitly specified, default to the current user's git config —
@<git-user.name>/dsh-plugin-<name>
. is reserved for the harness and must never be a plugin's own scope.
- Self-contained loading. A plugin must declare pointing at its own , whose lists the package's host row by package name. automatically appends any installed dependency that declares to the profile's — never hand-edit the profile's , , or a manifest just to load a plugin. Profile/home patches remain user override layers.
- Bundle-less packages are libraries. A package without still installs but activates no layer (dsh warns); reserve that shape for libraries plugins import, not for plugins.
- Ship runnable artifacts. must include the built host/client entries and . Git installs fetch sources, not build output: ship a self-contained script that builds the published entries without dev-only assumptions, or distribute built artifacts (npm / tarball). A user allowlisting a git is permitting install-time code, so they should pin a commit.
- Whole-row patches. A later patch replaces a row's entire by id — when overriding, restate every retained key. Users can override your rows in their profile patch without touching your package, so prefer configuration defaults they will keep.
- Client manifest matches the built bundle. must be ; must point at the built client bundle; edges are informational (preflight display / HMR diffing), not activation order — activation waits on service injection only.
- Pure core. imports no dsh package, so exercises the logic directly.
- No harness forks for one plugin. When a need crosses the plugin surface, follow the general-purpose proposal path below instead of patching or forking dsh ad hoc.
Develop → load → reload loop
- Edit host/client sources under .
- Build + typecheck + test: (emits the host ESM and the CJS client bundle), , .
- Install and load:
dsh plugin --profile <name> add link:<path>
(symlinks the package into the profile's ). Because the package declares , dsh appends it to the profile's and its own inserts the host row — the plugin is self-contained, with no manual profile patch or manifest edit. Remove it with dsh plugin --profile <name> remove <package>
(dependency and layer together).
- Reload: restart dsh. (While runs from the same checkout, client bundles hot-reload without a page refresh.)
is the dev-loop install; deployment uses
(copies into the profile), so the source checkout is not a runtime dependency.
The client bundle is CJS wrapped in
window.__ModuleLoader__.load({ id, factory })
;
and
imports are external (resolved from the loader's module table), relative imports are bundled.
Client surfaces
- Slots —
ctx.slots.register({ name, id, order, label, inject }, Component)
. Kinds: / / / ; scope: / / . Registering into an undeclared slot throws.
- Locale —
ctx.locale.register(ns, { zh, en })
then ; merge the key union into .
- Settings —
ctx.settingsScope.bind({ namespace, decode })
; persist via the RPC. A third-party namespace must be in or its writes are silently rejected.
- RPC — reach host domains through
ctx.get('connection').api.<domain>.<method>
(the apiproxy client face), not a custom transport.
Host surfaces
- Provide a service with + ; read optional deps with .
- Register settings with a schema via
ctx.settings.register(ns, schema, { applies })
.
Type & lint gotchas
exactOptionalPropertyTypes: true
: optional props need an explicit .
- CSS modules type as ; with , is — pass it with , never ( is a lint error in ).
- Render components with
React.createElement(Component, props)
, never — calling the function misattributes hooks (React error #310).
- The pre-commit hook lints with , so a type-aware rule's
// oxlint-disable-next-line
shows as an "unused directive" warning there; it's harmless and still required for the type-aware CI gate.
Proposing a change to dsh itself
A plugin should avoid forking the harness, but some needs (a slot, a wire field, a registry semantic) require a change to dsh. The change must be general-purpose — reusable beyond the one plugin that needs it, never a bespoke seam for it — and must fit the dsh / cordis design philosophy (plugin-first composition, explicit typed boundaries, minimal API surface). First search the existing discussions — dsh's GitHub Discussions and its Agent Notes tree — to check whether a similar proposal already exists; if so, build on it instead of duplicating.
For a new proposal, write a short discussion draft with this structure (sections only — mirror the existing drafts, don't hardcode them):
- Title —
# [Feature request] <short summary>
.
- English summary — a 1–2 sentence blockquote of the change and why.
- Background (the actual need I hit) — what plugin you're building, its scopes, and the concrete problem.
- Current state — quote the current code/comment/docs that explain the present behavior and why it's deliberate.
- Proposal — the minimal change, and why it's safe.
- Appendix: patch — the diff that implements it.
- Questions to confirm — the open questions for maintainers.
- Related — links to the docs and to related discussions/notes.