kcp-harness 0.1.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.
Files changed (46) hide show
  1. package/LICENSE +201 -0
  2. package/dist/audit.d.ts +73 -0
  3. package/dist/audit.js +142 -0
  4. package/dist/budget-ledger.d.ts +85 -0
  5. package/dist/budget-ledger.js +100 -0
  6. package/dist/classifier.d.ts +35 -0
  7. package/dist/classifier.js +177 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +177 -0
  10. package/dist/config.d.ts +65 -0
  11. package/dist/config.js +91 -0
  12. package/dist/downstream.d.ts +50 -0
  13. package/dist/downstream.js +167 -0
  14. package/dist/governor.d.ts +34 -0
  15. package/dist/governor.js +201 -0
  16. package/dist/index.d.ts +12 -0
  17. package/dist/index.js +21 -0
  18. package/dist/integrations/claude-code.d.ts +2 -0
  19. package/dist/integrations/claude-code.js +95 -0
  20. package/dist/integrations/cline.d.ts +2 -0
  21. package/dist/integrations/cline.js +70 -0
  22. package/dist/integrations/continue.d.ts +2 -0
  23. package/dist/integrations/continue.js +39 -0
  24. package/dist/integrations/copilot.d.ts +2 -0
  25. package/dist/integrations/copilot.js +69 -0
  26. package/dist/integrations/crush.d.ts +2 -0
  27. package/dist/integrations/crush.js +48 -0
  28. package/dist/integrations/cursor.d.ts +2 -0
  29. package/dist/integrations/cursor.js +65 -0
  30. package/dist/integrations/generate.d.ts +13 -0
  31. package/dist/integrations/generate.js +60 -0
  32. package/dist/integrations/openclaw.d.ts +2 -0
  33. package/dist/integrations/openclaw.js +67 -0
  34. package/dist/integrations/types.d.ts +51 -0
  35. package/dist/integrations/types.js +78 -0
  36. package/dist/integrations/windsurf.d.ts +2 -0
  37. package/dist/integrations/windsurf.js +67 -0
  38. package/dist/kcp-bridge.d.ts +2 -0
  39. package/dist/kcp-bridge.js +85 -0
  40. package/dist/proxy.d.ts +37 -0
  41. package/dist/proxy.js +394 -0
  42. package/dist/session.d.ts +50 -0
  43. package/dist/session.js +82 -0
  44. package/dist/temporal-watch.d.ts +63 -0
  45. package/dist/temporal-watch.js +147 -0
  46. package/package.json +56 -0
@@ -0,0 +1,167 @@
1
+ // Downstream MCP server management — spawn, initialize, and proxy to
2
+ // downstream MCP servers over stdio.
3
+ //
4
+ // The harness acts as an MCP client to each downstream server. It:
5
+ // 1. Spawns the server as a child process with stdio pipes
6
+ // 2. Performs the MCP initialization handshake
7
+ // 3. Collects the server's tool list
8
+ // 4. Routes tool calls to the appropriate downstream
9
+ // 5. Cleans up on shutdown
10
+ //
11
+ // Each downstream connection handles JSON-RPC 2.0 over newline-delimited
12
+ // stdio — the standard MCP framing. Request/response correlation uses
13
+ // monotonic integer IDs.
14
+ import { spawn } from "node:child_process";
15
+ import { createInterface } from "node:readline";
16
+ const HARNESS_VERSION = "0.1.0";
17
+ const INIT_TIMEOUT_MS = 15_000;
18
+ const CALL_TIMEOUT_MS = 60_000;
19
+ /** Manages the lifecycle of downstream MCP servers. */
20
+ export class DownstreamManager {
21
+ connections = [];
22
+ /** Map from tool name → downstream connection that owns it. */
23
+ toolOwners = new Map();
24
+ /** Spawn and initialize a downstream MCP server. */
25
+ async add(config) {
26
+ const conn = this.spawn(config);
27
+ await this.initialize(conn);
28
+ this.connections.push(conn);
29
+ // Register tool ownership
30
+ for (const tool of conn.tools) {
31
+ if (this.toolOwners.has(tool.name)) {
32
+ // Name collision — prefix with downstream name
33
+ const prefixed = `${config.name}__${tool.name}`;
34
+ this.toolOwners.set(prefixed, conn);
35
+ }
36
+ else {
37
+ this.toolOwners.set(tool.name, conn);
38
+ }
39
+ }
40
+ return conn;
41
+ }
42
+ /** Get all tools from all downstream servers. */
43
+ allTools() {
44
+ const tools = [];
45
+ for (const conn of this.connections) {
46
+ tools.push(...conn.tools);
47
+ }
48
+ return tools;
49
+ }
50
+ /** Find which downstream owns a tool. */
51
+ ownerOf(toolName) {
52
+ return this.toolOwners.get(toolName);
53
+ }
54
+ /** Call a tool on the downstream that owns it. */
55
+ async callTool(toolName, args) {
56
+ const conn = this.toolOwners.get(toolName);
57
+ if (!conn)
58
+ throw new Error(`no downstream owns tool: ${toolName}`);
59
+ if (!conn.alive)
60
+ throw new Error(`downstream ${conn.name} is not alive`);
61
+ const result = await this.rpc(conn, "tools/call", {
62
+ name: toolName,
63
+ arguments: args,
64
+ });
65
+ return result;
66
+ }
67
+ /** Shut down all downstream connections. */
68
+ async shutdown() {
69
+ for (const conn of this.connections) {
70
+ conn.alive = false;
71
+ conn.readline.close();
72
+ conn.process.kill("SIGTERM");
73
+ // Clear pending requests
74
+ for (const [, p] of conn.pending) {
75
+ clearTimeout(p.timer);
76
+ p.reject(new Error("downstream shutting down"));
77
+ }
78
+ conn.pending.clear();
79
+ }
80
+ this.connections = [];
81
+ this.toolOwners.clear();
82
+ }
83
+ // -- Internal lifecycle ---------------------------------------------------
84
+ spawn(config) {
85
+ const proc = spawn(config.command, config.args ?? [], {
86
+ stdio: ["pipe", "pipe", "pipe"],
87
+ env: { ...process.env, ...config.env },
88
+ });
89
+ if (!proc.stdout || !proc.stdin) {
90
+ throw new Error(`failed to spawn downstream ${config.name}: no stdio`);
91
+ }
92
+ const rl = createInterface({ input: proc.stdout, terminal: false });
93
+ const conn = {
94
+ name: config.name,
95
+ config,
96
+ process: proc,
97
+ readline: rl,
98
+ tools: [],
99
+ nextId: 1,
100
+ pending: new Map(),
101
+ alive: true,
102
+ };
103
+ // Wire up response handling
104
+ rl.on("line", (line) => {
105
+ const trimmed = line.trim();
106
+ if (!trimmed)
107
+ return;
108
+ try {
109
+ const msg = JSON.parse(trimmed);
110
+ if (msg.id !== undefined && msg.id !== null) {
111
+ const p = conn.pending.get(Number(msg.id));
112
+ if (p) {
113
+ conn.pending.delete(Number(msg.id));
114
+ clearTimeout(p.timer);
115
+ if (msg.error)
116
+ p.reject(new Error(msg.error.message));
117
+ else
118
+ p.resolve(msg.result);
119
+ }
120
+ }
121
+ }
122
+ catch {
123
+ // Ignore unparseable lines (stderr leaking into stdout, etc.)
124
+ }
125
+ });
126
+ proc.on("exit", () => {
127
+ conn.alive = false;
128
+ // Reject all pending requests
129
+ for (const [, p] of conn.pending) {
130
+ clearTimeout(p.timer);
131
+ p.reject(new Error(`downstream ${config.name} exited`));
132
+ }
133
+ conn.pending.clear();
134
+ });
135
+ return conn;
136
+ }
137
+ async initialize(conn) {
138
+ // MCP initialization handshake
139
+ const initResult = (await this.rpc(conn, "initialize", {
140
+ protocolVersion: "2025-06-18",
141
+ capabilities: {},
142
+ clientInfo: { name: "kcp-harness", version: HARNESS_VERSION },
143
+ }, INIT_TIMEOUT_MS));
144
+ // Send initialized notification
145
+ this.notify(conn, "notifications/initialized");
146
+ // List tools
147
+ const toolsResult = (await this.rpc(conn, "tools/list", {}, INIT_TIMEOUT_MS));
148
+ conn.tools = toolsResult.tools ?? [];
149
+ }
150
+ // -- JSON-RPC transport ---------------------------------------------------
151
+ rpc(conn, method, params, timeoutMs = CALL_TIMEOUT_MS) {
152
+ const id = conn.nextId++;
153
+ const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params });
154
+ conn.process.stdin.write(msg + "\n");
155
+ return new Promise((resolve, reject) => {
156
+ const timer = setTimeout(() => {
157
+ conn.pending.delete(id);
158
+ reject(new Error(`timeout waiting for ${method} from ${conn.name}`));
159
+ }, timeoutMs);
160
+ conn.pending.set(id, { resolve, reject, timer });
161
+ });
162
+ }
163
+ notify(conn, method, params) {
164
+ const msg = JSON.stringify({ jsonrpc: "2.0", method, params });
165
+ conn.process.stdin.write(msg + "\n");
166
+ }
167
+ }
@@ -0,0 +1,34 @@
1
+ import { type AgentPlan, type DecisionTrace } from "kcp-agent";
2
+ import type { GovernancePolicy } from "./config.js";
3
+ import type { Classification } from "./classifier.js";
4
+ import type { SessionState, ApprovedPlan } from "./session.js";
5
+ import type { SpendResult } from "./budget-ledger.js";
6
+ /** The governor's decision for a tool call. */
7
+ export interface GovernanceDecision {
8
+ /** Whether the tool call is approved. */
9
+ approved: boolean;
10
+ /** How the decision was made. */
11
+ mode: "plan-first" | "auto-plan" | "kcp-passthrough" | "blocked";
12
+ /** The plan that governs this decision (if any). */
13
+ plan?: AgentPlan;
14
+ /** The decision trace (if tracing is enabled). */
15
+ trace?: DecisionTrace;
16
+ /** Human-readable reason for the decision. */
17
+ reason: string;
18
+ /** The approved plan that matched (for plan-first mode). */
19
+ approvedPlan?: ApprovedPlan;
20
+ /** Budget spend result (for auto-plan mode with costs). */
21
+ budgetSpend?: SpendResult;
22
+ }
23
+ /**
24
+ * Govern a classified tool call — decide whether to approve or block.
25
+ *
26
+ * For KCP tools (kcp_plan, kcp_load, etc.), the call is always passed through
27
+ * to the kcp-agent planner directly — the harness doesn't gate KCP's own tools.
28
+ *
29
+ * For file/URL tools targeting governed domains, the governor checks:
30
+ * 1. Is there an existing approved plan that covers this path? → approve
31
+ * 2. If not, auto-plan against the domain's manifest → approve if selected
32
+ * 3. Otherwise → block (fail-closed)
33
+ */
34
+ export declare function govern(classification: Classification, toolName: string, args: Record<string, unknown>, session: SessionState, policy: GovernancePolicy): Promise<GovernanceDecision>;
@@ -0,0 +1,201 @@
1
+ // Governor — enforce governance via kcp-agent's deterministic planner.
2
+ //
3
+ // The governor is the enforcement layer. When the classifier identifies a
4
+ // tool call as knowledge-navigation, the governor decides whether to allow
5
+ // it by running the kcp-agent planner and checking the result.
6
+ //
7
+ // Two modes:
8
+ //
9
+ // 1. **Plan-first** (preferred): the agent has called kcp_plan/kcp_load and
10
+ // established an approved plan. Subsequent tool calls are checked against
11
+ // the plan's selected units. Fast — no planner call needed.
12
+ //
13
+ // 2. **Auto-plan** (fallback): the agent tries to access a governed path
14
+ // without a prior plan. The governor auto-creates a plan with the file
15
+ // path as the task, then checks eligibility. If the path's unit is
16
+ // selected → approve; if not → block.
17
+ //
18
+ // In both modes, the governor is fail-closed: if the planner can't reach
19
+ // the manifest, if the unit isn't selected, or if the budget is exhausted,
20
+ // the call is blocked — never silently passed through.
21
+ import { planTree, plans, } from "kcp-agent";
22
+ import { isPathApproved, addPlan, recordSpend } from "./session.js";
23
+ /**
24
+ * Govern a classified tool call — decide whether to approve or block.
25
+ *
26
+ * For KCP tools (kcp_plan, kcp_load, etc.), the call is always passed through
27
+ * to the kcp-agent planner directly — the harness doesn't gate KCP's own tools.
28
+ *
29
+ * For file/URL tools targeting governed domains, the governor checks:
30
+ * 1. Is there an existing approved plan that covers this path? → approve
31
+ * 2. If not, auto-plan against the domain's manifest → approve if selected
32
+ * 3. Otherwise → block (fail-closed)
33
+ */
34
+ export async function govern(classification, toolName, args, session, policy) {
35
+ // KCP tools pass through — they ARE the governance layer
36
+ if (toolName.startsWith("kcp_")) {
37
+ return { approved: true, mode: "kcp-passthrough", reason: "KCP tool — governance layer itself" };
38
+ }
39
+ if (!classification.governed || !classification.domain) {
40
+ return { approved: true, mode: "kcp-passthrough", reason: "ungoverned tool call" };
41
+ }
42
+ const domain = classification.domain;
43
+ const target = classification.target;
44
+ // Mode 1: check existing approved plans
45
+ if (target) {
46
+ const approved = isPathApproved(session, target);
47
+ if (approved) {
48
+ return {
49
+ approved: true,
50
+ mode: "plan-first",
51
+ plan: approved.plan,
52
+ approvedPlan: approved,
53
+ reason: `path ${target} is in approved plan for "${approved.task}"`,
54
+ };
55
+ }
56
+ }
57
+ // Mode 2: auto-plan — create a governance plan on the fly
58
+ if (target && domain.manifest) {
59
+ try {
60
+ const autoPlan = await autoGovern(target, domain, session, policy);
61
+ return autoPlan;
62
+ }
63
+ catch (e) {
64
+ const msg = e instanceof Error ? e.message : String(e);
65
+ // Fail-closed: planner error → block
66
+ if (policy.fail_closed) {
67
+ return {
68
+ approved: false,
69
+ mode: "blocked",
70
+ reason: `auto-plan failed (fail-closed): ${msg}`,
71
+ };
72
+ }
73
+ return {
74
+ approved: false,
75
+ mode: "blocked",
76
+ reason: `auto-plan failed: ${msg}`,
77
+ };
78
+ }
79
+ }
80
+ // No target extractable + governed domain → block
81
+ return {
82
+ approved: false,
83
+ mode: "blocked",
84
+ reason: `governed tool call with no extractable target — blocked by policy`,
85
+ };
86
+ }
87
+ /**
88
+ * Auto-govern: run the kcp-agent planner to decide if accessing a target
89
+ * path is approved. Creates a plan and checks if the target is in the
90
+ * selected set.
91
+ */
92
+ async function autoGovern(target, domain, session, policy) {
93
+ const followOptions = buildFollowOptions(policy, session);
94
+ // Use the target path as the task — the planner will score units
95
+ // against this and select the most relevant ones.
96
+ const task = `access ${target}`;
97
+ const tree = await planTree(domain.manifest, task, followOptions);
98
+ if (tree.error) {
99
+ return {
100
+ approved: false,
101
+ mode: "blocked",
102
+ reason: `manifest error: ${tree.error}`,
103
+ };
104
+ }
105
+ // Extract the flat plan list
106
+ const allPlans = Array.from(plans(tree));
107
+ const rootPlan = allPlans[0];
108
+ if (!rootPlan) {
109
+ return {
110
+ approved: false,
111
+ mode: "blocked",
112
+ reason: "planner returned no plan",
113
+ };
114
+ }
115
+ // Check: is the target path in the selected set?
116
+ const targetNorm = normalizePath(target);
117
+ const matchingUnit = rootPlan.selected.find((u) => u.loadEligible && pathOverlaps(targetNorm, u.path));
118
+ if (matchingUnit) {
119
+ // Register the plan in the session for future fast-path lookups
120
+ addPlan(session, domain.manifest, task, rootPlan);
121
+ // Track budget spend via ledger
122
+ let budgetSpend;
123
+ if (rootPlan.budget?.projectedSpend) {
124
+ const currency = rootPlan.budget.currency ?? "USDC";
125
+ budgetSpend = session.ledger.recordPlanSpend(domain.manifest, task, rootPlan.budget.projectedSpend, currency);
126
+ // Also update the legacy counter
127
+ recordSpend(session, rootPlan.budget.projectedSpend);
128
+ if (!budgetSpend.accepted) {
129
+ return {
130
+ approved: false,
131
+ mode: "auto-plan",
132
+ plan: rootPlan,
133
+ budgetSpend,
134
+ reason: `auto-plan blocked: budget ceiling exceeded — ${budgetSpend.reason}`,
135
+ };
136
+ }
137
+ }
138
+ // Register with temporal watcher
139
+ session.temporalWatch.register(domain.manifest, task, rootPlan, followOptions);
140
+ return {
141
+ approved: true,
142
+ mode: "auto-plan",
143
+ plan: rootPlan,
144
+ budgetSpend,
145
+ reason: `auto-plan approved: unit "${matchingUnit.id}" (score ${matchingUnit.score}) covers ${target}`,
146
+ };
147
+ }
148
+ // Check: was the unit selected but not load-eligible? (paywall, attestation, etc.)
149
+ const ineligibleUnit = rootPlan.selected.find((u) => !u.loadEligible && pathOverlaps(targetNorm, u.path));
150
+ if (ineligibleUnit) {
151
+ return {
152
+ approved: false,
153
+ mode: "auto-plan",
154
+ plan: rootPlan,
155
+ reason: `auto-plan blocked: unit "${ineligibleUnit.id}" covers ${target} but is not load-eligible (${ineligibleUnit.reasons.filter(r => r.startsWith("unaffordable") || r.startsWith("needs")).join("; ") || "gate restriction"})`,
156
+ };
157
+ }
158
+ // Target not in selected set → check if it was skipped and why
159
+ const skippedUnit = rootPlan.skipped.find((u) => pathOverlaps(targetNorm, u.id));
160
+ const skipReason = skippedUnit
161
+ ? `unit "${skippedUnit.id}" was skipped: ${skippedUnit.reason}`
162
+ : `no unit covers path ${target}`;
163
+ return {
164
+ approved: false,
165
+ mode: "auto-plan",
166
+ plan: rootPlan,
167
+ reason: `auto-plan blocked: ${skipReason}`,
168
+ };
169
+ }
170
+ /** Build FollowOptions from governance policy and session state. */
171
+ function buildFollowOptions(policy, session) {
172
+ const planOptions = {
173
+ maxUnits: policy.max_units,
174
+ strict: policy.strict,
175
+ env: policy.env,
176
+ };
177
+ if (policy.budget) {
178
+ planOptions.budget = {
179
+ amount: policy.budget.amount,
180
+ currency: policy.budget.currency,
181
+ spent: session.budgetSpent,
182
+ };
183
+ }
184
+ if (policy.context_budget !== undefined) {
185
+ planOptions.contextBudget = policy.context_budget;
186
+ }
187
+ return {
188
+ planOptions,
189
+ maxDepth: 0, // auto-plan doesn't follow federation by default
190
+ fetchGuard: {}, // default guards (no private hosts, https only)
191
+ };
192
+ }
193
+ function normalizePath(p) {
194
+ return p.replace(/\/+/g, "/").replace(/^\.\//, "");
195
+ }
196
+ /** Check if a target path overlaps with a unit path (loose match). */
197
+ function pathOverlaps(target, unitPath) {
198
+ const a = normalizePath(target);
199
+ const b = normalizePath(unitPath);
200
+ return a === b || a.endsWith("/" + b) || a.endsWith(b) || b.endsWith("/" + a);
201
+ }
@@ -0,0 +1,12 @@
1
+ export { classify, extractTargets, normalizePath, matchesPrefix, type Classification, } from "./classifier.js";
2
+ export { govern, type GovernanceDecision, } from "./governor.js";
3
+ export { AuditLog, InMemoryAuditLog, buildEvent, buildLifecycleEvent, buildBudgetEvent, buildDriftEvent, type AuditWriter, type AuditEvent, type AuditEventType, } from "./audit.js";
4
+ export { createSession, addPlan, isPathApproved, recordLoaded, getKnown, recordSpend, nextSequence, type SessionState, type ApprovedPlan, } from "./session.js";
5
+ export { DownstreamManager, type McpTool, type DownstreamConnection, } from "./downstream.js";
6
+ export { HarnessProxy, serveProxy, type ProxyOptions, } from "./proxy.js";
7
+ export { loadConfig, parseConfig, DEFAULT_POLICY, DEFAULT_AUDIT, type HarnessConfig, type GovernedDomain, type GovernancePolicy, type DownstreamConfig, type AuditConfig, } from "./config.js";
8
+ export { callKcpTool } from "./kcp-bridge.js";
9
+ export { BudgetLedger, type BudgetCeiling, type LedgerEntry, type LedgerSource, type LedgerCost, type LedgerSnapshot, type SpendResult, } from "./budget-ledger.js";
10
+ export { TemporalWatch, type WatchedPlan, type DriftResult, type WatchResult, } from "./temporal-watch.js";
11
+ export { generate, generateAll, listAgents, harnessServerEntry, governedPathsBlock, manifestRef, } from "./integrations/generate.js";
12
+ export { AGENTS, type AgentTarget, type IntegrationOutput, type IntegrationFile, type IntegrationOptions, type AgentInfo, } from "./integrations/types.js";
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ // kcp-harness — public API.
2
+ //
3
+ // The KCP Compliance Harness: an MCP proxy that enforces deterministic
4
+ // knowledge governance for any agent. The harness intercepts tool calls,
5
+ // classifies them as knowledge-navigation or pass-through, and routes
6
+ // governed calls through the kcp-agent planner before execution.
7
+ //
8
+ // Every decision is logged to an append-only audit log. The agent can't
9
+ // bypass governance because it only has access to the proxy's stdio.
10
+ export { classify, extractTargets, normalizePath, matchesPrefix, } from "./classifier.js";
11
+ export { govern, } from "./governor.js";
12
+ export { AuditLog, InMemoryAuditLog, buildEvent, buildLifecycleEvent, buildBudgetEvent, buildDriftEvent, } from "./audit.js";
13
+ export { createSession, addPlan, isPathApproved, recordLoaded, getKnown, recordSpend, nextSequence, } from "./session.js";
14
+ export { DownstreamManager, } from "./downstream.js";
15
+ export { HarnessProxy, serveProxy, } from "./proxy.js";
16
+ export { loadConfig, parseConfig, DEFAULT_POLICY, DEFAULT_AUDIT, } from "./config.js";
17
+ export { callKcpTool } from "./kcp-bridge.js";
18
+ export { BudgetLedger, } from "./budget-ledger.js";
19
+ export { TemporalWatch, } from "./temporal-watch.js";
20
+ export { generate, generateAll, listAgents, harnessServerEntry, governedPathsBlock, manifestRef, } from "./integrations/generate.js";
21
+ export { AGENTS, } from "./integrations/types.js";
@@ -0,0 +1,2 @@
1
+ import type { IntegrationOutput, IntegrationOptions } from "./types.js";
2
+ export declare function generateClaudeCode(options: IntegrationOptions): IntegrationOutput;
@@ -0,0 +1,95 @@
1
+ // Claude Code integration — MCP config + PreToolUse hooks + CLAUDE.md instructions.
2
+ import { harnessServerEntry, governedPathsBlock, manifestRef } from "./generate.js";
3
+ export function generateClaudeCode(options) {
4
+ const server = harnessServerEntry(options);
5
+ const manifest = manifestRef(options);
6
+ const paths = options.paths ?? ["docs/", "src/"];
7
+ const mcpJson = {
8
+ path: ".mcp.json",
9
+ content: JSON.stringify({
10
+ mcpServers: {
11
+ "kcp-harness": server,
12
+ },
13
+ }, null, 2) + "\n",
14
+ commitToGit: true,
15
+ description: "MCP server config — adds kcp-harness as a governance proxy",
16
+ };
17
+ const hooksJson = {
18
+ path: ".claude/settings.json",
19
+ content: JSON.stringify({
20
+ hooks: {
21
+ PreToolUse: [
22
+ {
23
+ matcher: "Read|Edit|Write|Glob|Grep",
24
+ hooks: [
25
+ {
26
+ type: "command",
27
+ command: "node",
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
+ }
36
+ `.trim()],
37
+ },
38
+ ],
39
+ },
40
+ ],
41
+ },
42
+ }, null, 2) + "\n",
43
+ commitToGit: true,
44
+ description: "PreToolUse hooks — block direct reads of governed paths, require kcp_load",
45
+ };
46
+ const claudeMd = {
47
+ path: "CLAUDE.md",
48
+ content: `# Knowledge Governance
49
+
50
+ This project uses the KCP Compliance Harness for deterministic knowledge governance.
51
+
52
+ ## Governed Paths
53
+
54
+ The following paths are governed by \`${manifest}\`:
55
+ ${governedPathsBlock(options)}
56
+
57
+ ## Rules
58
+
59
+ 1. **Always use \`kcp_load\`** before reading governed files — never read them directly
60
+ 2. Call \`kcp_plan\` first to see what units are available for your task
61
+ 3. The harness enforces budget ceilings and temporal validity automatically
62
+ 4. Use \`harness_status\` to check the current governance state
63
+ 5. Use \`harness_budget\` to check remaining budget
64
+ 6. Use \`harness_temporal_check\` to verify plans are still valid
65
+
66
+ ## Example Workflow
67
+
68
+ \`\`\`
69
+ 1. kcp_plan task="understand the API" manifest="${manifest}"
70
+ 2. kcp_load task="understand the API" manifest="${manifest}"
71
+ 3. Read governed files through the harness (auto-governed)
72
+ \`\`\`
73
+ `,
74
+ commitToGit: true,
75
+ description: "CLAUDE.md — instructions for using governed knowledge",
76
+ };
77
+ return {
78
+ agent: "claude-code",
79
+ name: "Claude Code",
80
+ files: [mcpJson, hooksJson, claudeMd],
81
+ instructions: `## Claude Code Integration
82
+
83
+ 1. The \`.mcp.json\` registers kcp-harness as an MCP server
84
+ 2. The \`.claude/settings.json\` adds PreToolUse hooks that block direct reads of governed paths
85
+ 3. The \`CLAUDE.md\` instructs the agent to use kcp_load for knowledge access
86
+
87
+ **Setup:**
88
+ \`\`\`bash
89
+ # Or add manually:
90
+ claude mcp add --scope project kcp-harness -- npx kcp-harness serve
91
+ \`\`\`
92
+
93
+ The hooks enforce governance at the tool level — even if the model tries to \`Read\` a governed path directly, the hook blocks it and redirects to kcp_load.`,
94
+ };
95
+ }
@@ -0,0 +1,2 @@
1
+ import type { IntegrationOutput, IntegrationOptions } from "./types.js";
2
+ export declare function generateCline(options: IntegrationOptions): IntegrationOutput;
@@ -0,0 +1,70 @@
1
+ // Cline integration — MCP settings snippet + .clinerules
2
+ import { harnessServerEntry, governedPathsBlock, manifestRef } from "./generate.js";
3
+ export function generateCline(options) {
4
+ const server = harnessServerEntry(options);
5
+ const manifest = manifestRef(options);
6
+ const mcpSnippet = {
7
+ path: "cline-mcp-snippet.json",
8
+ content: JSON.stringify({
9
+ "kcp-harness": {
10
+ ...server,
11
+ disabled: false,
12
+ autoApprove: [
13
+ "kcp_plan",
14
+ "kcp_trace",
15
+ "harness_status",
16
+ "harness_session",
17
+ "harness_budget",
18
+ "harness_temporal_check",
19
+ ],
20
+ },
21
+ }, null, 2) + "\n",
22
+ commitToGit: false,
23
+ description: "MCP snippet to add via Cline's MCP Servers settings UI",
24
+ };
25
+ const rulesFile = {
26
+ path: ".clinerules",
27
+ content: `# KCP Knowledge Governance
28
+
29
+ This project uses the KCP Compliance Harness for deterministic knowledge governance.
30
+
31
+ ## Governed Paths
32
+
33
+ ${governedPathsBlock(options)}
34
+
35
+ Governed by: \`${manifest}\`
36
+
37
+ ## Mandatory Rules
38
+
39
+ 1. **Always call \`kcp_load\`** before reading files in governed paths
40
+ 2. Call \`kcp_plan\` first to inspect the load plan
41
+ 3. The harness blocks ungoverned access to governed paths (fail-closed)
42
+ 4. Budget and temporal governance are enforced automatically
43
+
44
+ ## Available MCP Tools (kcp-harness server)
45
+
46
+ - \`kcp_plan\` — deterministic load plan
47
+ - \`kcp_load\` — plan + load content
48
+ - \`kcp_trace\` — 13-gate decision trace
49
+ - \`kcp_validate\` — lint knowledge.yaml
50
+ - \`harness_status\` — governance state
51
+ - \`harness_budget\` — spend tracking
52
+ - \`harness_temporal_check\` — drift detection
53
+ `,
54
+ commitToGit: true,
55
+ description: "Cline rules — governance instructions",
56
+ };
57
+ return {
58
+ agent: "cline",
59
+ name: "Cline",
60
+ files: [mcpSnippet, rulesFile],
61
+ instructions: `## Cline Integration
62
+
63
+ **Setup:**
64
+ 1. Open Cline sidebar > MCP Servers tab > gear icon
65
+ 2. Add the kcp-harness entry from \`cline-mcp-snippet.json\`
66
+ 3. Place \`.clinerules\` in your project root
67
+
68
+ The \`autoApprove\` list lets Cline call read-only governance tools without confirmation prompts. Write operations (kcp_load) still require approval.`,
69
+ };
70
+ }
@@ -0,0 +1,2 @@
1
+ import type { IntegrationOutput, IntegrationOptions } from "./types.js";
2
+ export declare function generateContinue(options: IntegrationOptions): IntegrationOutput;
@@ -0,0 +1,39 @@
1
+ // Continue integration — .continue/mcpServers/kcp-harness.yaml
2
+ import { manifestRef } from "./generate.js";
3
+ export function generateContinue(options) {
4
+ const manifest = manifestRef(options);
5
+ const harnessCmd = options.harnessCommand ?? "npx";
6
+ const harnessArgs = options.harnessArgs ?? [
7
+ "kcp-harness", "serve",
8
+ ...(options.harnessConfig ? ["--config", options.harnessConfig] : []),
9
+ ];
10
+ const mcpYaml = {
11
+ path: ".continue/mcpServers/kcp-harness.yaml",
12
+ content: `name: KCP Compliance Harness
13
+ version: 0.1.0
14
+ schema: v1
15
+ mcpServers:
16
+ - name: kcp-harness
17
+ command: ${harnessCmd}
18
+ args:
19
+ ${harnessArgs.map((a) => ` - "${a}"`).join("\n")}
20
+ `,
21
+ commitToGit: true,
22
+ description: "Continue MCP server config for kcp-harness",
23
+ };
24
+ return {
25
+ agent: "continue",
26
+ name: "Continue",
27
+ files: [mcpYaml],
28
+ instructions: `## Continue Integration
29
+
30
+ **Setup:**
31
+ 1. Place \`.continue/mcpServers/kcp-harness.yaml\` in your project root
32
+ 2. Restart Continue
33
+
34
+ Continue loads MCP servers from the \`.continue/mcpServers/\` directory automatically.
35
+ Use agent mode to access kcp-harness tools (\`kcp_plan\`, \`kcp_load\`, etc.).
36
+
37
+ Note: Continue uses YAML format (not JSON) for MCP server configs in the directory-based approach.`,
38
+ };
39
+ }
@@ -0,0 +1,2 @@
1
+ import type { IntegrationOutput, IntegrationOptions } from "./types.js";
2
+ export declare function generateCopilot(options: IntegrationOptions): IntegrationOutput;