dsh-tool-folder 0.1.2 → 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/lib/chainguard.js +89 -0
- package/lib/index.js +44 -0
- package/package.json +2 -2
|
@@ -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.
|
|
4
|
-
"description": "Fold the DSH tool surface per request
|
|
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",
|