Files
Reactive-Resume/packages/dsh-plugin/docs/2026-08-16-design.md
T
Amruth Pillai 65618a82a0 feat/dsh plugin (#3356)
* docs: remove .superpowers

* feat(dsh-plugin): bring the DeepSeek Harness plugin into the monorepo

Moves dsh-plugin-reactive-resume out of its own repository and into
packages/dsh-plugin. It stays a published, public npm package — the only
one here — but now builds, typechecks, tests, and lints under the same
turbo tasks as everything else.

The move pays for itself in the drift guard. Standalone, the plugin kept a
generated snapshot of the tool names scraped from the live server card at
https://rxresu.me, plus a weekly CI job to notice when that snapshot went
stale. Sitting next to packages/mcp, it reads MCP_TOOL_NAME directly, so a
tool rename breaks the prompt guide on the same pull request instead of
days later. The snapshot, the fetch script, and the scheduled job are gone.

packages/mcp gains a ./tool-names export so that import goes through the
public export map rather than another workspace's src.

Also flips autoInstallPeers off. The DeepSeek Harness rc packages declare
peers that are host-supplied and, in one case
(@deepseek-ai/dsh-type-meta), not published at all, so auto-install 404s
the whole workspace. Turning it off drops only optional peers elsewhere;
@neodrag/core was the single hard peer that had been arriving implicitly,
and it is now declared where it is used. Full typecheck and test suites
pass, and pnpm peers check reports nothing new beyond the pre-existing
drizzle-orm range mismatch.

Tests move from test/ to colocated src/*.test.ts and the build output from
lib/ to dist/ to match repository conventions.

* fix(dsh-plugin): ship a bundle manifest and target the current Harness

`dsh plugin add` warned that the package "declares no dsh.bundle — installed
as a plain dependency, not a profile layer", and it was right. Every other
Harness plugin, in-box and third-party, ships a cordis.patch.yml and points
dsh.bundle.patch at it; that declaration is what joins a package to a
profile's bundle stack. Without it the package installed and then sat inert,
and the README's hand-written insert row was a workaround for the gap rather
than the intended way in.

The peer ranges were also a generation behind. They asked for
@deepseek-ai/dsh-mcp-client and dsh-system-prompt at ^0.0.1-rc.1, which
cannot match the 0.1.0-rc.6 a current harness ships, so the plugin could
never have resolved against the thing it targets. Both APIs are unchanged
across the bump — StreamableHttpConfig still takes the same six fields and
PromptSection still takes name/order/text — so this is a range correction,
not a migration.

That bump pays for itself elsewhere. The old generation peer-depended on
@deepseek-ai/dsh-type-meta, which was never published, and working around
that 404 is why merging this package turned autoInstallPeers off for the
whole repository and pulled @neodrag/core in by hand. The new generation
dropped that peer and publishes every other one, so both changes are
reverted and pnpm-workspace.yaml is back to what it was.

Because a bundle patch mounts the plugin the moment it is installed, a
required apiKey would fail config validation and take the profile down
before the user ever had a chance to mint a key. It now defaults to empty
and apply() warns and mounts nothing, matching how dsh-honcho-memory
handles the same problem.

Verified by packing the tarball and installing it into a clean project with
default pnpm settings: it resolves, imports, and reports its exports.
2026-08-18 20:42:42 +02:00

10 KiB
Raw Blame History

DeepSeek Harness Plugin for Reactive Resume — Design

Date: 2026-08-16 Status: Approved, ready for implementation planning Package: dsh-plugin-reactive-resume

Post-implementation note: the tools config key and ctx.tools.restrict() call described below (Config, apply() step 2, "Tool-name generation and drift detection") were cut from v0.1.0. docs/spikes/2026-08-16-restrict-semantics.md found that restrict() cannot reach tools a plugin's own ctx.plugin(mcpClient, …) registers — it throws when called from an unscoped plugin context, and even from a real agent scope it refuses to touch a scope's own (as opposed to inherited) registrations. There is no arrangement reachable from this plugin's apply(ctx, config) where curation works, so 0.1.0 ships all tools unfiltered, per that spike's documented fallback. The rest of this document is left as originally written for history; it does not describe what shipped.

Goal

Distribute a DeepSeek Harness plugin that connects Harness to a Reactive Resume account, so a DSH user can read, create, and edit resumes and job applications from their agent session with one config row and an API key.

Context

What Reactive Resume already ships

Reactive Resume exposes a complete remote MCP server. Nothing on the server side needs to change for this plugin to work.

  • packages/mcp registers 33 tools (list_resumes, read_resume, apply_resume_patch, tailor_resume_for_application, …), 3 prompts, and 2 resources.
  • apps/server/src/http/app.ts mounts /mcp and /mcp/* (Streamable HTTP) plus /.well-known/mcp/server-card.json (SEP-1649).
  • apps/server/src/mcp/auth.ts accepts either an OAuth Authorization: Bearer token or an x-api-key header. The API key path needs no interactive flow.
  • The web app already ships API key management at /dashboard/settings/api-keys, so user provisioning is a solved problem.

What DeepSeek Harness provides

Verified against the published packages, not only the docs.

  • A plugin is a TypeScript module exporting name, optional inject, and apply(ctx, config). Config schemas use @deepseek-ai/schemastery.

  • @deepseek-ai/dsh-mcp-client (v0.0.1-rc.1) bridges one external MCP server per plugin instance. Its StreamableHttpConfig is exactly:

    interface StreamableHttpConfig {
      transport: 'streamable-http'
      serverName: string      // [A-Za-z0-9_-]{1,32}, unique across live instances
      url: string
      headers: Record<string, string>
      toolCallTimeoutMs: number
      failOnStartupError: boolean
    }
    

    Bridged tools become model-facing as mcp__<serverName>__<rawName>.

  • ctx.tools is a ToolRegistry exposing restrict(filter: ToolRestriction): () => void, where ToolRestriction is { allow?: readonly string[]; deny?: readonly string[] }.

  • ctx.systemPrompt (from @deepseek-ai/dsh-system-prompt) exposes section(section: PromptSection): () => void, where PromptSection is { name, order, text, complete? }. Convention: -100 is harness identity, 0 the deployment persona, and 100199 is tool guidance.

  • Plugins are distributed on npm and discovered through the dsh-plugin GitHub topic.

The gap this plugin fills

dsh-mcp-client has no tool filtering and no way to contribute prompt text. A user pasting a raw MCP row into cordis.yml gets all 33 tool schemas in their context budget and no guidance on Reactive Resume's JSON Patch semantics. Those two things are the plugin's reason to exist.

Decisions

Decision Choice Why
Scope Thin MCP bridge + prompt section The tool layer already exists and is maintained in Reactive Resume. Reimplementing 33 tool contracts against oRPC would drift every release.
Repo Standalone, outside the Reactive Resume monorepo The plugin imports nothing from Reactive Resume — it speaks HTTP. The monorepo has no npm publish pipeline: root and every package are private: true, there is no build output, no changesets, no NPM_TOKEN, and no publish workflow. Adding one to ship a dependency-free package is cost without benefit.
Auth x-api-key only Two clicks in Reactive Resume settings. OAuth needs a token store and an interactive flow for no gain.
Drift protection Server-card contract test in CI Replaces the lockstep the monorepo would have given, without the pipeline.

Architecture

Package shape

Namespace plugin, mirroring dsh-mcp-client's own export form:

export const name = 'reactive-resume'
export const inject = ['tools', 'systemPrompt']
export const Config: z<Config>
export async function apply(ctx: Context, config: Config): Promise<void>

@deepseek-ai/cordis, @deepseek-ai/dsh-tools, @deepseek-ai/dsh-system-prompt, and @deepseek-ai/dsh-mcp-client are peerDependencies so the plugin binds to the host's versions rather than installing a second copy of the runtime.

Config

interface Config {
  /** API key from <url>/dashboard/settings/api-keys. */
  apiKey: string
  /** Reactive Resume instance origin. Default 'https://rxresu.me'. */
  url?: string
  /** Tool namespace: tools appear as mcp__<serverName>__<rawName>. Default 'resume'. */
  serverName?: string
  /** Which tool group to expose. Default 'all'. */
  tools?: 'resume' | 'applications' | 'all'
  /** Per-tool-call timeout. Default inherited from dsh-mcp-client. */
  toolCallTimeoutMs?: number
}

url accepts any origin so self-hosted instances work unchanged.

apply()

Three steps, in order:

  1. Mount the bridge. ctx.plugin(mcpClient, { transport: 'streamable-http', serverName, url:${url}/mcp, headers: { 'x-api-key': apiKey }, toolCallTimeoutMs, failOnStartupError: true }). failOnStartupError: true turns a bad API key into a loud activation failure instead of tools that silently fail at call time.

  2. Curate the tool surface. When tools !== 'all', call ctx.tools.restrict({ deny: [...] }) with the namespaced names of the excluded group, computed from the generated tool-name list. See "Open risk" below.

  3. Contribute prompt guidance. ctx.systemPrompt.section({ name: 'reactive-resume', order: 150, text: PATCH_GUIDE }).

All three return disposers; Cordis effect scoping unwinds them on unload, so no manual cleanup is needed beyond returning them where the API expects it.

PATCH_GUIDE

The plugin's substance. Roughly 40 lines covering the failure modes Reactive Resume already encodes as error hints in packages/mcp/src/tools.ts (errorHint) — those hints exist precisely because models get these wrong:

  • Call read_resume before apply_resume_patch; never patch blind.
  • apply_resume_patch takes RFC-6902 operations against the resume data document.
  • Fetch the resume://_meta/schema resource before constructing paths.
  • Section entries are arrays of objects keyed by UUID; ids are not indices.
  • A locked resume rejects writes — call unlock_resume first.
  • list_resumes is the way to recover a valid id after a 404.

Written as static text, not a provider function — it does not vary per assembly.

Tool-name generation and drift detection

Superseded by the move into the monorepo. The plugin now sits beside the MCP server it bridges, so src/tool-names.test.ts reads @reactive-resume/mcp/tool-names directly. A tool rename fails that test on the same pull request. The generated snapshot, the fetch script, and the scheduled network job described below no longer exist.

  • Build step fetches <url>/.well-known/mcp/server-card.json and emits src/tool-names.generated.ts containing the raw tool names split into the resume and applications groups.
  • The generated file is committed, so npm install never needs network access.
  • A scheduled CI job refetches from https://rxresu.me and fails when the committed list no longer matches the live card. That failure is the signal to cut a new plugin release.

Install UX

README's copy-paste block:

- insert:
    - id: reactive-resume
      name: dsh-plugin-reactive-resume
      config:
        apiKey: !!js process.env.RXRESUME_API_KEY

Open risk

ToolRegistry.restrict's contract reads: "Per-scope filter over the tools a scope INHERITS — the global layer and every ancestor layer on its chain. Restrictions intersect, and do not affect the scope's own registrations."

ctx.plugin(mcpClient, …) mounts the bridge in a child scope of the plugin's context. The bridged tools are therefore registered in a descendant, not an ancestor, of the scope calling restrict. Whether a parent-scope restriction reaches them is unverified.

This must be settled by a spike before the config surface is committed, because tools is a public config key and removing it later is a breaking change.

Fallback if restrict does not reach the child scope: ship 0.1.0 without the tools key (equivalent to 'all'), and solve curation in 0.2.0 — possibly by mounting the bridge at the same scope level rather than as a child. The prompt section alone justifies the release.

Testing

  • Spike (blocking): run a local Reactive Resume (dotenvx run -f .env.local -- pnpm dev, port 3000), point a scratch Harness config at http://localhost:3000/mcp with an x-api-key, and confirm (a) the Streamable HTTP bridge connects and lists 33 tools, and (b) whether ctx.tools.restrict from the plugin's scope hides bridged tools.
  • Unit: Config schema defaults and validation; the deny-list computation from the generated tool names; PATCH_GUIDE section registers at order 150 with the expected name.
  • Contract: server-card fetch matches src/tool-names.generated.ts.
  • Manual smoke before publish: in a real Harness session against rxresu.me — list resumes, read one, apply a patch, verify the change in the web UI.

Out of scope

OAuth support, ctx.commands shortcuts, a bundled Harness skill, local resume caching, and PDF rendering inside Harness. Each is additive and none blocks a useful 0.1.0.

Build order

  1. Spike restrict() semantics and the Streamable HTTP bridge against localhost.
  2. Repo scaffold, Config schema, bridge mount, README. End-to-end working install.
  3. PATCH_GUIDE prompt section.
  4. Tool profiles, generated name list, drift CI.
  5. Publish 0.1.0, tag the repo with the dsh-plugin topic, submit to awesome-deepseek-harness.