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 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.4.1";
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 args = JSON.parse(process.env.TOOL_INPUT || '{}');
30
- const path = args.file_path || args.path || '';
31
- const governed = ${JSON.stringify(paths)}.some(p => path.startsWith(p) || path.includes('/' + p));
32
- if (governed) {
33
- console.log(JSON.stringify({ decision: "block", reason: "Use kcp_load to access governed knowledge at " + path }));
34
- process.exit(2);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kcp-harness",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "mcpName": "no.cantara/kcp-harness",
5
5
  "description": "KCP Compliance Harness — MCP proxy that enforces deterministic knowledge governance for any agent",
6
6
  "type": "module",