kcp-harness 0.1.0 → 0.3.0

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/README.md ADDED
@@ -0,0 +1,267 @@
1
+ # kcp-harness
2
+
3
+ **Deterministic knowledge governance for any AI agent.**
4
+
5
+ > *Your agent can read every file in your project. Can it prove why it read what it read?*
6
+
7
+ KCP Harness is an MCP compliance proxy that sits between an AI coding agent and its tools. It
8
+ intercepts knowledge-related calls, routes them through the
9
+ [kcp-agent](https://github.com/Cantara/kcp-agent) deterministic planner (13-gate cascade, no LLM),
10
+ and produces compliance artifacts — decision traces, audit logs, budget ledgers — as a side effect
11
+ of normal agent operation.
12
+
13
+ The agent can't bypass governance because it only talks to the proxy's MCP interface. The proxy
14
+ decides what knowledge is accessible, tracks spend, and logs every decision. **Fail-closed: if the
15
+ harness can't verify a request, the agent gets nothing.**
16
+
17
+ ```
18
+ Agent (Claude Code / Cursor / Copilot / Windsurf / Cline / Crush / OpenClaw / ...)
19
+
20
+ │ MCP tool call
21
+ v
22
+ ┌─────────────────────────────────────────────────────────┐
23
+ │ kcp-harness │
24
+ │ │
25
+ │ classify → govern (13 gates) → execute → audit │
26
+ │ │
27
+ │ Side outputs: │
28
+ │ · Decision traces (per-request, deterministic) │
29
+ │ · Audit log (append-only JSONL) │
30
+ │ · Budget ledger (itemized, ceiling-enforced) │
31
+ │ · Temporal drift (plan validity over time) │
32
+ └─────────────────────────────────────────────────────────┘
33
+
34
+ v
35
+ Knowledge manifests (knowledge.yaml)
36
+ ```
37
+
38
+ **[Documentation →](https://cantara.github.io/kcp-harness/)**
39
+
40
+ ## Why
41
+
42
+ Enterprises need agents that are *defensible* — auditable, reproducible, budget-controlled,
43
+ temporally pinned. Today's agents can't prove why they read what they read. The harness adds a
44
+ compliance layer without replacing the agent.
45
+
46
+ | What you keep | What the harness adds |
47
+ |---|---|
48
+ | Your agent (Claude Code, Cursor, Copilot, ...) | Deterministic knowledge selection |
49
+ | Your workflow (coding, reviewing, shipping) | Decision traces (13 gates per unit) |
50
+ | Your tools (MCP servers, shell, browser) | Budget enforcement (ceiling, per-currency) |
51
+ | | Temporal governance (drift detection) |
52
+ | | Append-only audit log |
53
+ | | Replay / cross-examination |
54
+
55
+ **You sell the compliance layer. The agents are pluggable.**
56
+
57
+ ## Install
58
+
59
+ ```bash
60
+ npm install -g kcp-harness
61
+ ```
62
+
63
+ Or use without installing:
64
+
65
+ ```bash
66
+ npx kcp-harness --help
67
+ ```
68
+
69
+ ### Native executables
70
+
71
+ Pre-built binaries (no Node/Deno required) for Linux x64/arm64, macOS x64/arm64, and Windows x64
72
+ — grab them from a [release](https://github.com/Cantara/kcp-harness/releases). To build one yourself:
73
+
74
+ ```bash
75
+ npm ci && npm run build
76
+ deno compile --allow-read --allow-env --allow-net --allow-run \
77
+ --node-modules-dir=auto --output kcp-harness dist/cli.js
78
+ ```
79
+
80
+ ## Quick start
81
+
82
+ ### 1. Initialize
83
+
84
+ ```bash
85
+ kcp-harness init # creates harness.yaml
86
+ ```
87
+
88
+ ### 2. Generate agent integration
89
+
90
+ ```bash
91
+ kcp-harness integrate claude-code # or: cursor, copilot, windsurf, cline, continue, crush, openclaw
92
+ ```
93
+
94
+ ### 3. Start coding
95
+
96
+ Your agent now routes knowledge access through the harness. Every decision is logged.
97
+
98
+ ## Supported agents
99
+
100
+ | Agent | Config | Integration |
101
+ |---|---|---|
102
+ | **Claude Code** | `.mcp.json` + PreToolUse hooks | `kcp-harness integrate claude-code` |
103
+ | **Cursor** | `.cursor/mcp.json` + `.mdc` rules | `kcp-harness integrate cursor` |
104
+ | **GitHub Copilot** | `.vscode/mcp.json` (uses `"servers"` key) | `kcp-harness integrate copilot` |
105
+ | **Windsurf** | global config + `.windsurfrules` | `kcp-harness integrate windsurf` |
106
+ | **Cline** | MCP settings + `.clinerules` | `kcp-harness integrate cline` |
107
+ | **Continue** | `.continue/mcpServers/*.yaml` | `kcp-harness integrate continue` |
108
+ | **Crush** | `crush.json` + PrepareStep | `kcp-harness integrate crush` |
109
+ | **OpenClaw** | `openclaw.json` + plugin hooks | `kcp-harness integrate openclaw` |
110
+
111
+ Each agent has its own MCP config format, rules file, and quirks. The `integrate` command handles
112
+ them all — one governance layer, any agent.
113
+
114
+ ## How it works
115
+
116
+ Every tool call flows through a five-stage pipeline:
117
+
118
+ ```
119
+ 1. RECEIVE MCP JSON-RPC request from agent
120
+ 2. CLASSIFY Knowledge-navigation or pass-through?
121
+ 3. GOVERN 13-gate cascade (audience → temporal → budget → ...)
122
+ 4. EXECUTE Call downstream tool / return content
123
+ 5. AUDIT Log decision to append-only audit log
124
+ ```
125
+
126
+ ### Classifier
127
+
128
+ The classifier examines each tool call and determines whether it targets governed knowledge.
129
+ `Read("docs/api.md")` where `docs/` is governed? Route through the planner. `Read("package.json")`
130
+ where `package.json` isn't governed? Pass through. KCP tools (`kcp_plan`, `kcp_load`) are always
131
+ governed.
132
+
133
+ ### Governor
134
+
135
+ Two modes:
136
+
137
+ - **Plan-first (fast path)** — the agent calls `kcp_plan` first. The harness caches the approved
138
+ plan. Subsequent reads are checked against the cached plan — no re-planning.
139
+ - **Auto-plan (fallback)** — the agent reads a governed path without planning. The harness runs
140
+ the planner automatically. Slower, but governance is enforced even for agents that don't know
141
+ about `kcp_plan`.
142
+
143
+ ### The 13-gate cascade
144
+
145
+ Every knowledge unit is evaluated through 13 deterministic gates, in order:
146
+
147
+ ```
148
+ audience → not_for → temporal → deprecated → supersession → relevance →
149
+ attestation → payment → access → strict → max_units → money_budget → context_budget
150
+ ```
151
+
152
+ A unit must pass **all** gates. The gate that blocks it is recorded in the decision trace. Same
153
+ inputs → same plan. No model involved.
154
+
155
+ ## MCP tools
156
+
157
+ Once connected, agents can use these governance tools:
158
+
159
+ | Tool | Description |
160
+ |---|---|
161
+ | `kcp_plan` | Deterministic load plan — which units, in what order, which skipped and why |
162
+ | `kcp_load` | Plan + load eligible unit content |
163
+ | `kcp_trace` | Full 13-gate decision trace |
164
+ | `kcp_validate` | Lint a `knowledge.yaml` |
165
+ | `harness_status` | Current governance state |
166
+ | `harness_budget` | Itemized spend tracking |
167
+ | `harness_temporal_check` | Plan drift detection |
168
+
169
+ ## Compliance artifacts
170
+
171
+ ### Audit log
172
+
173
+ Append-only JSONL. Every decision — governed or pass-through — is logged with sequence number,
174
+ timestamp, tool, targets, and governance decision:
175
+
176
+ ```bash
177
+ cat .kcp-harness/audit.jsonl | jq 'select(.governed == true)'
178
+ ```
179
+
180
+ ### Budget ledger
181
+
182
+ Append-only itemized spend tracking. Per-currency running totals. Ceiling enforcement — a load
183
+ that would exceed the budget is rejected atomically (no partial loads).
184
+
185
+ ### Temporal governance
186
+
187
+ Plans are registered with a temporal watcher. On subsequent calls, the watcher re-evaluates
188
+ against the current time. If units have drifted (expired, newly valid), the harness emits a drift
189
+ event. Long-running sessions stay honest.
190
+
191
+ ## Configuration
192
+
193
+ ```yaml
194
+ # harness.yaml
195
+ version: "1.0"
196
+
197
+ governance:
198
+ domains:
199
+ - manifest: "./knowledge.yaml"
200
+ paths: ["docs/", "src/"]
201
+
202
+ policy:
203
+ fail_closed: true
204
+ audit_all: true
205
+ max_units: 5
206
+ budget:
207
+ amount: 1.00
208
+ currency: USDC
209
+
210
+ audit:
211
+ path: ".kcp-harness/audit.jsonl"
212
+ ```
213
+
214
+ ## CLI
215
+
216
+ ```
217
+ kcp-harness serve [--config harness.yaml] Start the MCP proxy
218
+ kcp-harness init Create a harness.yaml template
219
+ kcp-harness check [--config harness.yaml] Validate configuration
220
+ kcp-harness integrate <agent> [options] Generate agent integration files
221
+ kcp-harness integrate --list List supported agents
222
+ ```
223
+
224
+ ## Library
225
+
226
+ ```ts
227
+ import { classify, govern, BudgetLedger, TemporalWatch } from "kcp-harness";
228
+ import { generate, listAgents } from "kcp-harness";
229
+
230
+ // Classify a tool call
231
+ const result = classify("Read", { file_path: "docs/api.md" }, governedDomains);
232
+
233
+ // Generate integration files
234
+ const output = generate("claude-code", { manifest: "./knowledge.yaml", paths: ["docs/"] });
235
+ ```
236
+
237
+ ## Architecture
238
+
239
+ ```
240
+ ┌──────────────────────────────────────────────┐
241
+ │ Layer 3: Integration Packages │
242
+ │ Agent-specific configs + rules files │
243
+ │ (claude-code, cursor, copilot, ...) │
244
+ ├──────────────────────────────────────────────┤
245
+ │ Layer 2: KCP Compliance Harness │ ← THIS
246
+ │ MCP proxy — deterministic governance │
247
+ ├──────────────────────────────────────────────┤
248
+ │ Layer 1: kcp-agent (planner core) │
249
+ │ 13-gate cascade, decision traces │
250
+ └──────────────────────────────────────────────┘
251
+ ```
252
+
253
+ **Forking agents puts you in competition. A harness puts you in composition.**
254
+
255
+ ## Tests
256
+
257
+ ```bash
258
+ npm test # 123 tests across 9 test files
259
+ ```
260
+
261
+ Covers: classifier (28), audit (14), session (9), governor (5), proxy (10), integration (9),
262
+ budget-ledger (14), temporal-watch (6), integrations (28).
263
+
264
+ ## License
265
+
266
+ Apache-2.0 · By [eXOReaction AS](https://www.exoreaction.com), hosted under
267
+ [Cantara](https://github.com/Cantara).
package/dist/audit.d.ts CHANGED
@@ -71,3 +71,5 @@ export declare function buildBudgetEvent(sessionId: string, sequence: number, ac
71
71
  }): AuditEvent;
72
72
  /** Build a temporal drift event. */
73
73
  export declare function buildDriftEvent(sessionId: string, sequence: number, drift: DriftResult): AuditEvent;
74
+ /** Redact sensitive values from tool arguments for audit logging. */
75
+ export declare function sanitizeArgs(toolName: string, args: Record<string, unknown>): Record<string, unknown>;
package/dist/audit.js CHANGED
@@ -106,7 +106,7 @@ export function buildDriftEvent(sessionId, sequence, drift) {
106
106
  };
107
107
  }
108
108
  /** Redact sensitive values from tool arguments for audit logging. */
109
- function sanitizeArgs(toolName, args) {
109
+ export function sanitizeArgs(toolName, args) {
110
110
  const sanitized = { ...args };
111
111
  // Redact content from Write calls (could be large/sensitive)
112
112
  if (toolName === "Write" && sanitized["content"]) {
@@ -25,6 +25,14 @@ export class BudgetLedger {
25
25
  /** Record a spend event. Returns whether it was accepted. */
26
26
  record(source, cost) {
27
27
  const currency = cost.currency || "USDC";
28
+ // Reject negative amounts — no refunds through the ledger
29
+ if (cost.amount < 0) {
30
+ return {
31
+ accepted: false,
32
+ total: this.totals.get(currency) ?? 0,
33
+ reason: `negative spend rejected: ${cost.amount} ${currency}`,
34
+ };
35
+ }
28
36
  const currentTotal = this.totals.get(currency) ?? 0;
29
37
  const newTotal = round6(currentTotal + cost.amount);
30
38
  // Check ceiling
@@ -29,7 +29,7 @@ export declare function extractTargets(toolName: string, args: Record<string, un
29
29
  paths: string[];
30
30
  urls: string[];
31
31
  };
32
- /** Normalize a file path: strip leading ./ and trailing /, collapse //. */
32
+ /** Normalize a file path: resolve ../, strip leading ./, collapse //. */
33
33
  export declare function normalizePath(p: string): string;
34
34
  /** Check if a path starts with a prefix (directory-boundary-aware). */
35
35
  export declare function matchesPrefix(path: string, prefix: string): boolean;
@@ -60,7 +60,7 @@ export function classify(toolName, args, domains) {
60
60
  }
61
61
  }
62
62
  // Rule 3: extract path and match against governed path prefixes
63
- const pathExtractor = PATH_EXTRACTORS[toolName];
63
+ const pathExtractor = Object.hasOwn(PATH_EXTRACTORS, toolName) ? PATH_EXTRACTORS[toolName] : undefined;
64
64
  if (pathExtractor) {
65
65
  const target = pathExtractor(args);
66
66
  if (target) {
@@ -82,7 +82,7 @@ export function classify(toolName, args, domains) {
82
82
  }
83
83
  }
84
84
  // Rule 4: extract URL and match against governed URL prefixes
85
- const urlExtractor = URL_EXTRACTORS[toolName];
85
+ const urlExtractor = Object.hasOwn(URL_EXTRACTORS, toolName) ? URL_EXTRACTORS[toolName] : undefined;
86
86
  if (urlExtractor) {
87
87
  const target = urlExtractor(args);
88
88
  if (target) {
@@ -130,12 +130,26 @@ export function extractTargets(toolName, args) {
130
130
  function str(v) {
131
131
  return typeof v === "string" ? v : undefined;
132
132
  }
133
- /** Normalize a file path: strip leading ./ and trailing /, collapse //. */
133
+ /** Normalize a file path: resolve ../, strip leading ./, collapse //. */
134
134
  export function normalizePath(p) {
135
135
  let n = p.replace(/\/+/g, "/");
136
136
  if (n.startsWith("./"))
137
137
  n = n.slice(2);
138
- // Don't strip leading / absolute paths stay absolute
138
+ // Resolve ../ segments to prevent traversal bypass
139
+ const parts = n.split("/");
140
+ const resolved = [];
141
+ for (const part of parts) {
142
+ if (part === "..") {
143
+ if (resolved.length > 0 && resolved[resolved.length - 1] !== "..") {
144
+ resolved.pop();
145
+ }
146
+ // else: leading ../ — keep it (can't resolve beyond root)
147
+ }
148
+ else if (part !== ".") {
149
+ resolved.push(part);
150
+ }
151
+ }
152
+ n = resolved.join("/");
139
153
  return n;
140
154
  }
141
155
  /** Check if a path starts with a prefix (directory-boundary-aware). */
@@ -170,8 +184,20 @@ function extractPathPrefix(pattern) {
170
184
  function extractBashTarget(command) {
171
185
  if (!command)
172
186
  return undefined;
173
- // Match common file-access commands: cat, head, tail, less, more, vi, nano
174
- const fileCommands = /\b(?:cat|head|tail|less|more|vi|vim|nano|code)\s+["']?([^\s"'|;&]+)/;
187
+ // Match common file-access commands: read, copy, move, link, archive
188
+ const fileCommands = /\b(?:cat|head|tail|less|more|vi|vim|nano|code|cp|mv|ln|tar|zip|scp|rsync)\s+(?:-[^\s]*\s+)*["']?([^\s"'|;&]+)/;
175
189
  const m = command.match(fileCommands);
176
- return m?.[1] ?? undefined;
190
+ if (m?.[1])
191
+ return m[1];
192
+ // Match redirect targets: > or >> followed by a path
193
+ const redirect = /[>]\s*["']?([^\s"'|;&]+)/;
194
+ const r = command.match(redirect);
195
+ if (r?.[1])
196
+ return r[1];
197
+ // Match programming language file access: python/node/ruby -c '...open("path")...'
198
+ const langRead = /\bopen\s*\(\s*["']([^"']+)["']\s*\)/;
199
+ const l = command.match(langRead);
200
+ if (l?.[1])
201
+ return l[1];
202
+ return undefined;
177
203
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kcp-harness",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "KCP Compliance Harness — MCP proxy that enforces deterministic knowledge governance for any agent",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,6 +11,7 @@
11
11
  },
12
12
  "files": [
13
13
  "dist",
14
+ "README.md",
14
15
  "LICENSE"
15
16
  ],
16
17
  "scripts": {