kcp-harness 0.5.0 → 0.5.1
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/dist/cli.js +1 -1
- package/dist/integrations/claude-code.js +88 -8
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -36,7 +36,7 @@ governance for any agent. It intercepts tool calls, classifies them
|
|
|
36
36
|
as knowledge-navigation or pass-through, and routes governed calls
|
|
37
37
|
through the kcp-agent planner before execution.
|
|
38
38
|
`;
|
|
39
|
-
const VERSION = "0.
|
|
39
|
+
const VERSION = "0.5.1";
|
|
40
40
|
const TEMPLATE = `# kcp-harness configuration
|
|
41
41
|
version: "1.0"
|
|
42
42
|
|
|
@@ -20,19 +20,99 @@ export function generateClaudeCode(options) {
|
|
|
20
20
|
hooks: {
|
|
21
21
|
PreToolUse: [
|
|
22
22
|
{
|
|
23
|
-
matcher: "Read|Edit|Write|Glob|Grep",
|
|
23
|
+
matcher: "Bash|Read|Edit|Write|Glob|Grep",
|
|
24
24
|
hooks: [
|
|
25
25
|
{
|
|
26
26
|
type: "command",
|
|
27
27
|
command: "node",
|
|
28
28
|
args: ["-e", `
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
29
|
+
const deny = (reason) => {
|
|
30
|
+
console.log(JSON.stringify({
|
|
31
|
+
hookSpecificOutput: {
|
|
32
|
+
hookEventName: "PreToolUse",
|
|
33
|
+
permissionDecision: "deny",
|
|
34
|
+
permissionDecisionReason: reason,
|
|
35
|
+
},
|
|
36
|
+
}));
|
|
37
|
+
process.exit(0);
|
|
38
|
+
};
|
|
39
|
+
// Path matching mirrors src/classifier.ts so native file tools can't slip past
|
|
40
|
+
// with a Bash command, a Glob/Grep pattern, a case variant, or a backslash
|
|
41
|
+
// separator. Plain string ops only — this runs as an inline node script.
|
|
42
|
+
const BS = String.fromCharCode(92);
|
|
43
|
+
const GOV = ${JSON.stringify(paths)}.map((p) => {
|
|
44
|
+
let b = String(p).split(BS).join("/").toLowerCase();
|
|
45
|
+
while (b.slice(-1) === "/") b = b.slice(0, -1);
|
|
46
|
+
return b;
|
|
47
|
+
}).filter(Boolean);
|
|
48
|
+
const norm = (p) => {
|
|
49
|
+
let n = String(p == null ? "" : p).split(BS).join("/");
|
|
50
|
+
while (n.indexOf("//") >= 0) n = n.split("//").join("/");
|
|
51
|
+
if (n.slice(0, 2) === "./") n = n.slice(2);
|
|
52
|
+
const parts = n.split("/"), out = [];
|
|
53
|
+
for (let i = 0; i < parts.length; i++) {
|
|
54
|
+
const s = parts[i];
|
|
55
|
+
if (s === "..") { if (out.length && out[out.length - 1] !== "..") out.pop(); }
|
|
56
|
+
else if (s !== "." && s !== "") out.push(s);
|
|
57
|
+
}
|
|
58
|
+
return out.join("/").toLowerCase();
|
|
59
|
+
};
|
|
60
|
+
const isGoverned = (t) => {
|
|
61
|
+
if (!t) return false;
|
|
62
|
+
const n = norm(t);
|
|
63
|
+
return GOV.some((b) => n === b || n.indexOf(b + "/") === 0 || n.indexOf("/" + b + "/") >= 0);
|
|
64
|
+
};
|
|
65
|
+
const globPrefix = (pattern) => {
|
|
66
|
+
const s = String(pattern || "");
|
|
67
|
+
let idx = -1;
|
|
68
|
+
for (let i = 0; i < s.length; i++) { if ("*?[{".indexOf(s[i]) >= 0) { idx = i; break; } }
|
|
69
|
+
if (idx <= 0) return "";
|
|
70
|
+
const pre = s.slice(0, idx), ls = pre.lastIndexOf("/");
|
|
71
|
+
if (ls > 0) return pre.slice(0, ls);
|
|
72
|
+
if (pre.length > 0 && pre.indexOf(".") < 0) { let r = pre; while (r.slice(-1) === "/") r = r.slice(0, -1); return r; }
|
|
73
|
+
return "";
|
|
74
|
+
};
|
|
75
|
+
const tokens = (cmd) => {
|
|
76
|
+
const s = String(cmd || ""), seps = " " + String.fromCharCode(9) + String.fromCharCode(10) + ";|&<>()" + String.fromCharCode(34) + String.fromCharCode(39);
|
|
77
|
+
let cur = "", out = [];
|
|
78
|
+
for (let i = 0; i < s.length; i++) { const ch = s[i]; if (seps.indexOf(ch) >= 0) { if (cur) { out.push(cur); cur = ""; } } else cur += ch; }
|
|
79
|
+
if (cur) out.push(cur);
|
|
80
|
+
return out;
|
|
81
|
+
};
|
|
82
|
+
const bashTarget = (cmd) => {
|
|
83
|
+
const toks = tokens(cmd);
|
|
84
|
+
const readCmds = ["cat","head","tail","less","more","vi","vim","nano","code","cp","mv","ln","tar","zip","scp","rsync"];
|
|
85
|
+
for (let i = 0; i < toks.length; i++) {
|
|
86
|
+
if (readCmds.indexOf(toks[i]) >= 0) {
|
|
87
|
+
for (let j = i + 1; j < toks.length; j++) { if (toks[j][0] !== "-" && isGoverned(toks[j])) return toks[j]; }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (String(cmd || "").indexOf(">") >= 0) { for (let j = 0; j < toks.length; j++) { if (isGoverned(toks[j])) return toks[j]; } }
|
|
91
|
+
return "";
|
|
92
|
+
};
|
|
93
|
+
let raw = "";
|
|
94
|
+
process.stdin.on("data", (d) => { raw += d; });
|
|
95
|
+
process.stdin.on("end", () => {
|
|
96
|
+
let input;
|
|
97
|
+
try {
|
|
98
|
+
input = JSON.parse(raw);
|
|
99
|
+
} catch {
|
|
100
|
+
// Fail-closed: an unreadable payload means we cannot prove the target is
|
|
101
|
+
// ungoverned, so we must not let it through.
|
|
102
|
+
deny("kcp-harness: could not parse PreToolUse input — failing closed");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const tool = input.tool_name || "";
|
|
106
|
+
const a = input.tool_input || {};
|
|
107
|
+
let targets;
|
|
108
|
+
if (tool === "Glob" || tool === "Grep") targets = [a.path || "", globPrefix(a.pattern)];
|
|
109
|
+
else if (tool === "Bash") targets = [bashTarget(a.command)];
|
|
110
|
+
else targets = [a.file_path || a.path || ""];
|
|
111
|
+
const hit = targets.filter(Boolean).find(isGoverned);
|
|
112
|
+
if (hit) {
|
|
113
|
+
deny("Use kcp_load to access governed knowledge at " + hit);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
36
116
|
`.trim()],
|
|
37
117
|
},
|
|
38
118
|
],
|
package/package.json
CHANGED