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 +267 -0
- package/dist/audit.d.ts +2 -0
- package/dist/audit.js +1 -1
- package/dist/budget-ledger.js +8 -0
- package/dist/classifier.d.ts +1 -1
- package/dist/classifier.js +33 -7
- package/package.json +2 -1
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"]) {
|
package/dist/budget-ledger.js
CHANGED
|
@@ -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
|
package/dist/classifier.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/classifier.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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": {
|