dsh-tool-folder
Verifieddsh-tool-folder ยท v0.1.8 ยท MIT
Fold the DSH tool surface per request + ChainGuard firewall (high-risk block + exfil-chain detection). Shrinks schema tokens 80-90% while keeping selection accuracy. v0.1.6 adds a zod Config schema so every toggle renders in the DSH settings UI.
Install
dsh plugin add dsh-tool-folder Confirm the layer applied with dsh --profile default --dump-config โ see the install guide.
Source
Published to npm without a public repository. Inspect the package contents before installing.
Tags
Readme
dsh-tool-folder
Fold the DSH tool surface down to what each request actually needs. Per-agent core + BM25 dynamic loading + heat feedback + CN-intent aliases. Shrinks per-turn schema tokens ~80-90% while keeping (or improving) tool selection accuracy โ the RAG-MCP / BoR / SEP-1576 methodology, implemented as a plain cordis plugin for the DeepSeek Harness host.
Mechanism (verified against DSH 0.1.1-rc.1 sources)
SystemPrompt.assemble() โ assembly = { sections, contexts, tools, variables }
ctx.waterfall(scope, "system-prompt/assemble", assembly, ctx, () => assembly)
โ this plugin listens here and returns {...assembly, tools: filtered}
buildRequest(turn, step, assembly.tools, ...) โ tools field of the API request
assembly.tools is the ONLY source of the API request's tools. Returning a
replaced object is authoritative (model-selection plugin does the same).
Execution is NOT gated on the current turn's tools โ folded tools still
resolve through the full registry โ so folding never removes capability.
Feedback and the firewall bind to the execution events, not the prompt assembly:
tools/pre-executeโ listener receives(exec, next). Return{ kind: "deny", reason }to refuse the call, or callnext()to allow.exec = { callId, name, arguments, agent, signal }(there is no.deny()method on exec โ the contract is the returned decision object).tools/post-executeโ listener receives(exec, result, next)and fires once per executed tool;exec.name/exec.argumentsfeed heat counting, exfil-chain detection, coverage recording and session persistence. (agent/pre-stepcarries{ messages, position, signal }only โ no toolCalls โ so it cannot drive feedback; verified against dsh-agent-loop.)
Six layers โ all implemented
| Layer | Mechanism | Status |
|---|---|---|
| L1 schema compression | conservative description/param trimming (no $ref โ provider-incompatible), compressEnabled |
โ dyn segment only |
| L2 dynamic loading | per-agent core + BM25 top-K + CN-alias server route | โ core |
| L3 selection metrics | selectionCoverage per session + offline retrievalMetrics |
โ recorded to feedback.json |
| L4 execution side | folded tools stay executable + heat promotion + tools_search tool |
โ |
| L5 cache discipline | deterministic ordering (core by config, hot/dyn by name) = byte-stable prefix | โ |
Three-segment layout
| Segment | Content | Cost |
|---|---|---|
| core | per-agent core + heat-promoted tools, full schema | small, constant |
| dyn | BM25 top-K for the current query + CN-alias server top-K | small, per request |
| folded | everything else โ dropped from the schema | zero |
An alias hit no longer pulls the whole server in: the matched server's tools
are ranked by BM25 (with the matched alias keywords bridged into the query to
cross the CN/EN gap) and the top min(3, serverSize) are loaded. When the
query yields no lexical signal at all, a stable name-order subset is loaded so
the alias still helps.
Optionally (catalogEnabled: true, default) folded tools are listed as a
one-line catalog section so the model still knows they exist and can call them
by name (execution-side fallback still works).
Install
Official (needs the dsh CLI / pnpm):
dsh plugin --profile desktop add file:E:/DSH-Data/dsh-tool-folder
# or, after publishing: dsh plugin --profile desktop add github:<owner>/dsh-tool-folder
Manual fallback: put the package where the profile loader can resolve
dsh-tool-folder, then add to profiles/<name>/cordis.patch.yml:
- insert:
- id: tool-folder
name: 'dsh-tool-folder'
config:
enabled: true
core: [tool-bash, tool-pwsh]
topK: 6
hotThreshold: 3
(Manual install is not yet end-to-end verified โ the loader resolution path for a locally-added bundle is the one open question.)
Rollback
Set enabled: false (or remove the insert row) and restart DSH. Folded tools
were never unregistered, so nothing else changes.
Config
| key | default | meaning |
|---|---|---|
| enabled | true | master switch |
| perAgent | {} | {agentId: {core: [...]}} overrides |
| core | [] | always-loaded tools (fallback for unknown agents) |
| deny | [] | never inject + refuse execution: exact name, or prefix* matches a whole server. Deny wins over core, include and the catalog too. |
| topK | 6 | BM25 dynamic segment size |
| hotThreshold | 3 | folded tool called N times โ auto-promote to core |
| catalogEnabled | true | append one-line catalog section for folded tools (fold safety net) |
| schemaToolEnabled | true | register the tools_schema meta tool (full parameter schema of one tool by name) |
| compressLevel | off | L1 compression tier: off | light | standard | aggressive. standard trims descriptions to 200 chars + param descriptions to 120; aggressive additionally drops optional parameters (required โ properties guaranteed constructively). Applies to dyn segment only โ core/hot never trimmed. |
| compressEnabled | false | deprecated compat: compressLevel: off + compressEnabled: true is treated as standard |
| normalizeDescriptions | false | dyn-segment description hygiene: injection-marker removal + whitespace normalization + first-sentence keep + 300-char cap |
| category | {} | intent routing { "่ฎฐๅฟ/ๅๅฟ/remember": ["mcp__viking"], ... }. Key words are /-separated (OR). Category wins over aliases; each matched server loads per-server top-3. |
| include | [] | whitelist: exact name or prefix*. Non-empty โ only matching tools are injected (meta tools exempt; deny still wins). Execution is NOT gated by include. |
| toonifyResults | false | compact long JSON results after execution (drop empty fields + truncate strings; only when the text block is >2000 chars and parses as JSON) |
| maxFoldMs | 50 | hard cap; beyond this keep the full list |
| feedbackFile | '' | feedback JSON path (default $DSH_HOME/logs/tool-folder/) |
| aliases | built-in | CN-intent keyword โ server prefix table (per-server top-K) |
Files
lib/bm25.jsโ zero-dep BM25 (k1=1.2, b=0.75; CN bigrams + EN tokens)lib/schema.jsโ L1 tiered compression (compressLevel) + description normalization + lossless-JSON sanitizerlib/category.jsโ P1-1 intent-category routing (pure function, unit-testable)lib/toonify.jsโ P2-2 long JSON result compaction (pure function)lib/index.jsโ assemble hook, three-segment fold, heat feedback, safetycordis.patch.ymlโ bundle insert declarationtest.jsโ simulated-cordis harness:node test.js
Known limits
- BM25 cannot cross the CN-query / EN-description language gap on its own; the alias table covers common CN intents and now bridges its EN keywords into the per-server BM25 scoring. A future query-rewrite leg (ARK LLM, proven in the GPT Researcher smart retriever) removes this entirely.
- Alias hits load the server's top-3 relevant tools, not the whole server. A
server whose most relevant tool ranks low on a bridged CN query may miss it โ
the
tools_searchmeta-tool and heat promotion are the recovery paths. catalogEnabledrender behavior needs one runtime verification pass (section render is verified; the catalog text itself is standard).toonifyResultsmutation of the result object in the host's post-execute waterfall is not yet proven end-to-end (the listener's return is a gate, not the result body) โ the pure function + unit tests are in; runtime propagation needs one verification pass. Default off until then.aggressivecompression hides optional parameters from the model, so a model that would have used them can't (execution still validates against the registry's original schema โ C6).standardis the safe default tier;aggressiveis opt-in.
Changelog
v0.1.6 โ settings UI schema (2026-08-25)
Configschema exported (schemastery): every toggle now renders as a native form in the DSH settings UI โ no more hand-editing YAML. The schema mirrorsDEFAULTSone-to-one (18 fields: enabled / core / deny / topK / hotThreshold / catalogEnabled / compressLevel / compressEnabled / schemaToolEnabled / normalizeDescriptions / category / include / toonifyResults / toolSearchEnabled / firewallEnabled / maxFoldMs / feedbackFile / aliases), with per-field Chinese descriptions and range constraints (topK 0-50, hotThreshold โฅ1, maxFoldMs 0-1000). Invalid values are rejected with precise messages ("expected off | light | standard | aggressive but got X"). Same pattern as @deepseek-ai/dsh-tool-todo (z.object+.default(), schemastery~standardbridge).- Dependencies:
@deepseek-ai/schemastery+zod(bothdependencies, matching the official plugin pattern โ the zod adapter is required by the~standardbridge).
v0.1.5 โ tools_schema + tiered compression + intent routing (2026-08-25)
- P0-1
tools_schemameta tool: model-driven full-schema discovery for one tool by name (complement totools_search). Output is{schema:{type:"json"}}(host contract C3 โ a strict object schema would reject arbitrary tool schemas with "is not a declared property"). Its return value is deep-washed bysanitizeLossless(C4:const:-0/NaN can never leak). Meta tools are now managed by a sharedMETA_TOOLSblock that keeps bothtools_searchandtools_schemaalways visible. - P0-2
compressLeveltiers:light/standard/aggressivereplace the single conservative trim.aggressivedrops optional parameters and rebuildspropertiesfromrequired, sorequired โ propertiesholds constructively (C8). Required properties are never dropped;$ref/oneOf/itemsdeep structure is never touched; non object-root parameter schemas get description-only trimming.compressEnabled: trueremains as a deprecated compat alias forstandard. - P1-1
categoryintent routing:{ "่ฎฐๅฟ/ๅๅฟ": ["mcp__viking"] }routes a query to explicit server prefixes (per-server top-3, BM25-bridged) and wins over the alias table. Default{}= zero behavior change. - P1-2
normalizeDescriptions: dyn-segment descriptions are sanitized (prompt-injection markers removed), whitespace-collapsed, first-sentence kept, and capped at 300 chars. Core/hot descriptions are never touched. - P2-1
includewhitelist: exact name orprefix*; non-empty โ only matching tools enter the injection pool. Meta tools are exempt (a whitelist can never hide the discovery tools), deny still wins, and execution is never gated by include (C5). - P2-2
toonifyResults(default off): post-execute compaction of long JSON text blocks (>2000 chars) โ drop empty fields, truncate long strings. Any parse/compact failure leaves the original result untouched. Pure function inlib/toonify.js; host waterfall propagation still to be confirmed at runtime (see Known limits). - Tests: 33 โ comprehensive suite (~50+ checks) covering every new feature plus regression guards for the red lines ($ref untouched, core untouched, required โ properties, lossless JSON, deny-wins-over-include).
v0.1.4 โ audit fixes (2026-08-25)
- P0 firewall contract fixed (
tools/pre-execute): the listener now returns{ kind: "deny", reason }to refuse (host contract verified:prepareExecutionreadsgate.kind/gate.reason;exechas{ callId, name, arguments, agent, signal }โ the oldpayload.deny()call andtoolName/argvfields never existed, so high-risk blocks silently never fired). - P0 feedback loop revived (
tools/post-execute): heat counting, exfil-chain recording, coverage and session persistence moved offagent/pre-step(whose payload has no toolCalls) ontotools/post-execute(fires per executed tool withexec.name/exec.arguments). Feedback now actually persists and heat-promotes across restarts. - P0 ESM crash removed:
require("node:fs")under"type": "module"threw ReferenceError and silently killed all feedback persistence; replaced with top-levelimport fs/import path. Also fixed a parameter-shadow bug (pathparam hid thenode:pathmodule inpersistFeedback). - P1 alias whole-server pull fixed: alias hits now load a per-server
BM25 top-K (
min(3, serverSize)) with alias keywords bridged into the query, instead of dragging in every tool of a matched server. - P1 deny config added:
deny: [](exact name orprefix*) removes a tool from the injected surface AND refuses its execution attools/pre-execute. Deny wins over core and the catalog. - P2 stopword over-removal fixed:
list/get/set/make/use(ing)removed from the EN stopword set โ they are tool-name/description high-frequency terms (list_sessions,get_config) and were eroding retrieval signal. - P2 -0 defense: metrics now normalize
-0โ0(DSH lossless JSON rejects-0). - P2 default flip:
catalogEnableddefaults totrue(fold safety net โ the model can see that hidden tools exist).compressEnabledstays off (L1 not fully verified). - Tests: suite extended 15 โ 29 checks, covering the deny-return
contract,
tools/post-executereceivingexec.name, deny config (inject + execution), alias per-server top-K, no-requireESM hygiene, and no--0metric outputs. - NEW-1 (QA finding, LOW): three backslash-sensitive firewall regexes
(
HKLM\SAM,\system32\config,\Run) silently never matched when argv reachedverdict()JSON-encoded (single backslash โ\\). Changed\\to\\+so both raw-string and JSON-encoded paths are blocked. Added 4 regression checks (suite now 33).
v0.1.3 โ initial release (2026-08-24)
Quality check (2026-08-24, real 45-tool set)
Full-pipeline test with the REAL tool list (converted to DSH public names
mcp__<server>__<tool>), 7 typical queries:
| query | result | verdict |
|---|---|---|
| delegate a coding task to openhands | 7 openhands tools | โ |
| ๅธฎๆ่ฎฐไฝ่ฟไธชไบๅฎ / ๆฅ่ฎฐๅฟ ่ฎฐไฝ ๅๅฟ | top-3 viking tools (alias, per-server top-K) | โ |
| fetch library documentation | 2 context7 tools | โ |
| start a learning session | top-3 deeptutor tools (alias, per-server top-K) | โ |
| ๅคๆๆจ็ไปปๅก ๆทฑๅบฆๆ่ | deeptutor deep_* + reasonix + openhands | โ |
| ๆ็ดขไปฃ็ semantic search | viking grep/glob + reasonix code | โ |
- Fold ratio: 45 tools โ 2-18 visible per query (avg ~8) โ -82% schema tokens
- v0.1.4: alias hits are capped at the server's top-3 by relevance instead of pulling the whole server in (the v2 per-server top-K refinement is now live).
- BM25 shows minor lexical noise on some queries (e.g.
cancel_watchon an openhands query) โ the model filters this during synthesis.
Bugs found & fixed during the check
- tools_search self-hide (real bug): the meta-tool was folded by its own filter, so the model could never see it. Now always kept visible.
- Test-data error (not a code bug): raw MCP tool names vs DSH public
names (
mcp__<server>__<tool>, verified in dsh-mcp-clientpublicToolName). The plugin's prefix logic was correct all along.