dsh-tool-folder 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cordis.patch.yml CHANGED
@@ -16,13 +16,21 @@
16
16
  # Tools always kept in full (schema) for every agent. These are the
17
17
  # platform essentials; everything else is folded and BM25-loaded.
18
18
  core:
19
- - tool-bash
20
- - tool-pwsh
19
+ - read
20
+ - write
21
+ - edit
22
+ - glob
23
+ - grep
24
+ - pwsh
25
+ - web_search
26
+ - skill
27
+ - ask_user_question
28
+ - subagent
21
29
  # Per-agent overrides. agent ids come from context.agent.id; agents
22
30
  # not listed here fall back to `core`. Example:
23
31
  # perAgent:
24
32
  # standard:
25
- # core: [tool-bash, tool-pwsh, mcp__viking__remember]
33
+ # core: [read, write, edit, mcp__viking__remember]
26
34
  perAgent: {}
27
35
  # BM25 dynamic segment size per request.
28
36
  topK: 6
@@ -0,0 +1,89 @@
1
+ // ChainGuard — session-level tool-call firewall (ported from xiaowan tool_firewall T3).
2
+ //
3
+ // Two layers:
4
+ // 1. ChainGuard.check() — high-risk command verdict (AEGIS-style hard block list).
5
+ // 2. ChainGuard.record() — session window of tool calls; ChainGuard.checkChain()
6
+ // detects "sensitive read → external exfil" chains (Invariant-style: a single
7
+ // call is harmless, the SEQUENCE is the attack).
8
+ //
9
+ // Zero dependencies, pure regex + window buffer. Runs in the cordis plugin sandbox.
10
+
11
+ const HIGH_RISK_PATTERNS = [
12
+ // destructive filesystem
13
+ [/rm\s+-rf\s+[\\/]?[A-Za-z]:[\\/]|rm\s+-rf\s+\/|rm\s+-rf\s+[*?]/i, "recursive root delete"],
14
+ [/format\s+[A-Za-z]:|format\s+\/q/i, "disk format"],
15
+ [/del\s+\/f\s+\/s\s+\/q/i, "force recursive delete"],
16
+ // download → execute chain
17
+ [/(wget|curl|powershell|pwsh).{0,80}(\||;|&&).{0,30}(sh|bash|powershell|pwsh|cmd|start)\b/i, "download-pipe-execute"],
18
+ [/invoke-expression|iex\s*\(|iwr\s+.*\|.*iex/i, "powershell download-exec"],
19
+ // credential / exfil
20
+ [/(net\s+user|whoami\s+\/all|reg\s+query\s+HKLM\\SAM)/i, "credential harvest"],
21
+ [/copy\s+.*\\system32\\config|type\s+.*\\(ntuser\.dat|SAM|SYSTEM)/i, "system file exfil"],
22
+ // shadow / persistence
23
+ [/schtasks\s+\/create|reg\s+add\s+.*\\run/i, "persistence install"],
24
+ ];
25
+
26
+ // Tools that READ sensitive local state (file system, memory, secrets).
27
+ const SENSITIVE_READ = new Set([
28
+ "read", "glob", "grep", "edit",
29
+ "mcp__viking__find", "mcp__viking__glob", "mcp__viking__read",
30
+ "ssh_download", "ssh_list", "job_output", "describe_image", "read_image",
31
+ ]);
32
+
33
+ // Tools that SEND data to an external surface (network, memory writes, uploads).
34
+ const EXTERNAL_SEND = new Set([
35
+ "mcp__viking__remember", "mcp__viking__add_resource", "mcp__viking__forget",
36
+ "ssh_upload", "send_message", "web_search",
37
+ "mcp__openhands__openhands_create_conversation",
38
+ ]);
39
+
40
+ export function verdict(argvText) {
41
+ if (!argvText) return null;
42
+ for (const [re, label] of HIGH_RISK_PATTERNS) {
43
+ if (re.test(argvText)) {
44
+ return { blocked: true, label, pattern: String(re) };
45
+ }
46
+ }
47
+ return { blocked: false };
48
+ }
49
+
50
+ export class ChainGuard {
51
+ constructor({ window = 30 } = {}) {
52
+ this.window = window;
53
+ this.history = []; // [{tool, argv, ts}]
54
+ }
55
+
56
+ record(tool, argv = "") {
57
+ this.history.push({ tool, argv: String(argv || "").slice(0, 300), ts: Date.now() });
58
+ if (this.history.length > this.window) {
59
+ this.history.splice(0, this.history.length - this.window);
60
+ }
61
+ }
62
+
63
+ // Sensitive read followed within 3 steps by an external send → exfil chain.
64
+ checkChain() {
65
+ if (this.history.length < 2) return null;
66
+ for (let i = 0; i < this.history.length; i++) {
67
+ const h = this.history[i];
68
+ if (!SENSITIVE_READ.has(h.tool)) continue;
69
+ const lookahead = this.history.slice(i + 1, i + 4);
70
+ for (let j = 0; j < lookahead.length; j++) {
71
+ if (EXTERNAL_SEND.has(lookahead[j].tool)) {
72
+ return {
73
+ risk: "high",
74
+ label: "sensitive-read → external-send chain",
75
+ chain: [
76
+ `${h.tool}:${String(h.argv).slice(0, 60)}`,
77
+ ...lookahead.slice(0, j + 1).map((x) => `${x.tool}:${String(x.argv).slice(0, 60)}`),
78
+ ],
79
+ };
80
+ }
81
+ }
82
+ }
83
+ return null;
84
+ }
85
+
86
+ reset() {
87
+ this.history = [];
88
+ }
89
+ }
package/lib/index.js CHANGED
@@ -24,6 +24,7 @@
24
24
  import { buildIndex, search } from "./bm25.js";
25
25
  import { compressTool } from "./schema.js";
26
26
  import { selectionCoverage } from "./metrics.js";
27
+ import { ChainGuard, verdict } from "./chainguard.js";
27
28
  import { defineTool } from "@deepseek-ai/dsh-tools";
28
29
 
29
30
  export const name = "tool-folder";
@@ -38,6 +39,7 @@ const DEFAULTS = {
38
39
  catalogEnabled: false,
39
40
  compressEnabled: false,
40
41
  toolSearchEnabled: true,
42
+ firewallEnabled: true, // ChainGuard: high-risk hard block + exfil chain warn
41
43
  maxFoldMs: 50,
42
44
  feedbackFile: "",
43
45
  aliases: {
@@ -129,6 +131,7 @@ export function apply(ctx, config) {
129
131
  let allTools = [];
130
132
  let lastVisible = { agentId: "", names: [], ts: 0 };
131
133
  const disposers = [];
134
+ const chain = new ChainGuard({ window: 30 }); // firewall: exfil-chain + high-risk
132
135
 
133
136
  const serverOf = (name) => name.split("__")[0];
134
137
 
@@ -307,6 +310,23 @@ export function apply(ctx, config) {
307
310
  changed = true;
308
311
  }
309
312
 
313
+ // Firewall: record the call sequence and flag exfil chains.
314
+ if (cfg.firewallEnabled !== false) {
315
+ for (const n of names) chain.record(n);
316
+ const exfil = chain.checkChain();
317
+ if (exfil) {
318
+ logger.warn(
319
+ "[firewall] %s: %s",
320
+ exfil.label, exfil.chain.join(" → "),
321
+ );
322
+ feedback.firewall = feedback.firewall || [];
323
+ feedback.firewall.push({ ts: Date.now(), ...exfil });
324
+ if (feedback.firewall.length > 20) {
325
+ feedback.firewall.splice(0, feedback.firewall.length - 20);
326
+ }
327
+ }
328
+ }
329
+
310
330
  // Folded-call detection: did the model call something we hid?
311
331
  const cov = selectionCoverage(lastVisible.names, names);
312
332
  if (cov.uncovered.length > 0) {
@@ -335,6 +355,30 @@ export function apply(ctx, config) {
335
355
  }),
336
356
  );
337
357
 
358
+ // Firewall hard-block: deny high-risk command patterns before execution.
359
+ if (cfg.firewallEnabled !== false) {
360
+ disposers.push(
361
+ ctx.on("tools/pre-execute", (payload, next) => {
362
+ try {
363
+ const toolName = payload?.toolName;
364
+ const argv = payload?.argv;
365
+ const argvText = Array.isArray(argv) ? argv.join(" ") : String(argv || "");
366
+ const v = verdict(argvText);
367
+ if (v?.blocked && typeof payload?.deny === "function") {
368
+ logger.warn("[firewall] BLOCKED %s: %s", toolName, v.label);
369
+ payload.deny(
370
+ `[tool-folder firewall] 高危命令已拦截(${v.label})。如需执行请人工确认。`,
371
+ );
372
+ return; // deny() aborts this execution
373
+ }
374
+ } catch (e) {
375
+ logger.warn("firewall hook error (%s) — best-effort", e?.message);
376
+ }
377
+ return next?.();
378
+ }),
379
+ );
380
+ }
381
+
338
382
  // Cleanup on plugin unload: no leaked listeners.
339
383
  return () => {
340
384
  for (const d of disposers) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-tool-folder",
3
- "version": "0.1.1",
4
- "description": "Fold the DSH tool surface per request: per-agent core + BM25 dynamic loading + heat feedback. Shrinks schema tokens 80-90% while keeping selection accuracy.",
3
+ "version": "0.1.3",
4
+ "description": "Fold the DSH tool surface per request + ChainGuard firewall (high-risk block + exfil-chain detection). Shrinks schema tokens 80-90% while keeping selection accuracy.",
5
5
  "keywords": [
6
6
  "dsh-plugin",
7
7
  "dsh",