dsh-tool-folder
Đã xác minhdsh-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.
Cài đặt
dsh plugin add dsh-tool-folder Xác nhận layer đã áp bằng dsh --profile default --dump-config — xem hướng dẫn cài plugin.
Mã nguồn
Phát hành lên npm mà không có repository công khai. Hãy kiểm tra nội dung package trước khi cài.
Thẻ
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.