Skip to content

feat: add DeepSeek Harness (DSH) native plugin adapter - #792

Open
redtidev1918 wants to merge 1 commit into
DietrichGebert:mainfrom
redtidev1918:dsh-plugin-adapter
Open

feat: add DeepSeek Harness (DSH) native plugin adapter#792
redtidev1918 wants to merge 1 commit into
DietrichGebert:mainfrom
redtidev1918:dsh-plugin-adapter

Conversation

@redtidev1918

@redtidev1918 redtidev1918 commented Sep 2, 2026

Copy link
Copy Markdown

What

Adds a native DeepSeek Harness (DSH) adapter — the native-plugin tier of the existing adapters (Hermes Agent, Grok Build, Codex). DSH is a Cordis-based agent harness whose community plugin ecosystem is indexed via the dsh-plugin GitHub topic / awesome-dsh-plugin.com; there is currently no ponytail plugin there.

The adapter is a self-contained bundle in .dsh-plugin/: one cordis.patch.yml mounts a Node plugin (272 lines, zero dependencies, no install scripts) that

  • registers the six /ponytail* slash commands, and
  • serves the six skills through a skill provider in the skill registry's global layer.

Why global-layer skills (not customSkillDirs)

The obvious approach — pointing the built-in dsh-skill-filesystem at the bundle's skills/ via a patch config — is inert under the web profile: the host skill-filesystem row is disabled there because skill discovery moves to per-agent preset layers. A provider registered on the global (deployment) layer instead merges into every agent's catalog (documented in dsh-web-app/cordis.patch.yml; the skills do reach a booted agent's catalog — verified end-to-end in a headless boot).

Precedence is deliberately the lowest: candidates use BUNDLED_SKILL_RANK (600 — the host's rank scale is project 100/200, custom 300, user 400/500, bundled 600), and the registry merges preset-local layers over the global layer ([global, ...chainLayers(scope)], later wins), so a user's own same-named skill wins either way.

Commands steer the agent

A DSH command handler runs without opening a model turn, and its result text is rendered only in the UI — it never enters model history (deriveEventMessage projects only user/message, assistant/message, tool/result). So each handler drives behavior with agent.steer(createUserMessage(...)), the same pattern the built-in /plan command uses. createUserMessage is lazy-imported from the host package (@deepseek-ai/dsh-llm, declared in peerDependencies — the convention dsh-git-rollback uses for host packages), with a minimal-shaped fallback message for dev/link layouts where the host package is not on the module-resolution path.

/ponytail takes lite|full|ultra|off case-insensitively; unknown levels fall back to full instead of being echoed into the prompt.

Robustness

  • A bundled SKILL.md with a non-kebab name or missing name/description is skipped with a warning rather than surfaced: the host validates candidates outside the per-provider try/catch, so one bad file would otherwise take down the whole profile's skill registry.
  • An unreadable bundle dir degrades to an empty provider + warning instead of throwing at boot.
  • Frontmatter parsing normalizes CRLF: this repo has no .gitattributes, so a Windows checkout (core.autocrlf) stores SKILL.md with \r\n and npm packs whatever is on disk — without normalization all six skills would silently disappear on such machines.
  • Shared skill definitions are frozen at load, so a consumer can't corrupt state served to every agent.
  • No watchers (skills are static inside the package); the ceiling is noted with a ponytail: comment.

Security surface

Runtime plugin: zero dependencies, no install scripts, no eval, network, child-process, or file writes; it reads only files inside the package's own skills/ directory (paths are built from readdir Dirent.name, no traversal). Two independent adversarial audits with fresh contexts, treating the plugin as third-party code on user machines: the first returned FIX-FIRST (commands were inert — the issue this PR's design section addresses — plus missing kebab-name validation and /off edge cases); after fixes, a re-audit returned SHIP. Post-audit hardening (CRLF handling, explicit node:crypto import, definition freezing, docs alignment) was reviewed manually against host source — the third-party audit tool was unavailable for that final pass, so I state plainly it was a manual review, not a fresh audit.

Governance (matches the existing adapters)

  • tests/dsh-plugin.test.js (node:test, picked up by the root npm test glob) — 9 tests covering: bundle manifest + dsh.bundle declaration; the DSH registry candidate/definition contract; that every command handler steers a user message with an id (the one field the host inbox enforces); /off and case/unknown-level handling; kebab-name rejection; CRLF parsing; missing-dir degradation; folded-block frontmatter; and byte-identity of the bundled skills/ copies.
  • scripts/check-versions.js now guards .dsh-plugin/package.json (all version files pinned to the same version).
  • README.md + docs/agent-portability.md document the adapter.
  • .dsh-plugin/skills/ copies are refreshed by .dsh-plugin/scripts/sync-skills.mjs (byte-checked by the test).

Verification

  • node --test tests/dsh-plugin.test.js → 9/9 pass.
  • Full repo suite → 93/93 with pandas installed (a throwaway venv matching CI's pip install pandas). Without pandas locally it is 92/93: the one failure (csv: correct pandas one-liner) also reproduces on a clean tree without our changes — purely the missing pandas, verified by re-running the suite with pandas present.
  • node scripts/check-versions.js → all 9 version files pinned at 4.9.0.
  • npm pack --dry-run → 11 files, ~14 kB, lib + skills + patch + manifests only.
  • End-to-end in a booted headless profile: all six skills are enumerated and loadable by the agent. Slash commands cannot be exercised via the headless CLI (its argument is passed as plain task text), so they are verified by unit tests — every handler steers a user message with an id — plus construction matching the built-in /plan pattern.

Install

# from a checkout of this repo:
dsh plugin --profile web add /absolute/path/to/ponytail/.dsh-plugin

# once the npm package exists:
dsh plugin --profile web add dsh-plugin-ponytail

Happy to adjust naming, wiring, or anything else to fit your conventions. I deliberately did not publish the dsh-plugin-ponytail npm name (verified free on the registry) — it stays available for you to publish as the first-party package.

A self-contained DSH bundle in .dsh-plugin/: one cordis patch mounts a
zero-hard-dependency Node plugin that registers the six /ponytail* slash
commands and a global-layer skill provider serving .dsh-plugin/skills/.

Commands steer the agent:
- a DSH command handler runs without opening a model turn and its result
  text is rendered only in the UI (never enters model history), so each
  handler drives behavior with agent.steer(createUserMessage(...)), the
  same pattern as the built-in /plan command; it lazy-imports
  createUserMessage from the host (@deepseek-ai/dsh-llm, declared as a
  peerDependency) with a minimal-shaped fallback for dev layouts where the
  host package is not on the resolution path
- /ponytail takes lite|full|ultra|off case-insensitively; unknown levels
  fall back to full instead of being echoed

Skills reach every agent/preset through the skill registry global layer
(the web profile disables the host skill-filesystem row; preset-local rows
merge over global, so a customSkillDirs override would be inert under web):
- candidates use BUNDLED_SKILL_RANK (600, lowest precedence) so a
  user/project skill of the same name always wins
- a bundled SKILL.md with a non-kebab name or missing name/description is
  skipped with a warning instead of being surfaced: the host validates
  candidates outside the per-provider try/catch, so one bad file would
  otherwise take down the whole profile skill registry
- an unreadable skills dir degrades to an empty provider with a warning

Governance, matching the existing adapters:
- tests/dsh-plugin.test.js (node:test) covers the bundle manifest, the DSH
  registry candidate/definition contract, that every handler steers a user
  message with an id, kebab-name rejection, missing-dir degradation, folded
  frontmatter parsing, and byte-identity of the bundled skills copies
- scripts/check-versions.js guards the new package.json version
- README and docs/agent-portability.md document the adapter
- skills copies refreshed by scripts/sync-skills.mjs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant