kcp-harness 0.1.0 → 0.4.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-reader.d.ts +68 -0
- package/dist/audit-reader.js +150 -0
- package/dist/audit.d.ts +5 -0
- package/dist/audit.js +2 -1
- package/dist/budget-ledger.js +8 -0
- package/dist/classifier.d.ts +1 -1
- package/dist/classifier.js +33 -7
- package/dist/cli.js +65 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.js +2 -0
- package/dist/dashboard/server.d.ts +30 -0
- package/dist/dashboard/server.js +145 -0
- package/dist/dashboard/tail.d.ts +18 -0
- package/dist/dashboard/tail.js +77 -0
- package/dist/dashboard/ui.d.ts +1 -0
- package/dist/dashboard/ui.js +193 -0
- package/dist/export.d.ts +28 -0
- package/dist/export.js +226 -0
- package/dist/governor.d.ts +3 -1
- package/dist/governor.js +18 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,8 @@ import { dirname, join } from "node:path";
|
|
|
14
14
|
import { loadConfig } from "./config.js";
|
|
15
15
|
import { serveProxy } from "./proxy.js";
|
|
16
16
|
import { generate, listAgents } from "./integrations/generate.js";
|
|
17
|
+
import { exportEvidence } from "./export.js";
|
|
18
|
+
import { DashboardServer } from "./dashboard/server.js";
|
|
17
19
|
const USAGE = `kcp-harness — KCP Compliance Harness
|
|
18
20
|
|
|
19
21
|
Usage:
|
|
@@ -22,6 +24,8 @@ Usage:
|
|
|
22
24
|
kcp-harness check [--config harness.yaml] Validate configuration
|
|
23
25
|
kcp-harness integrate <agent> [options] Generate agent integration files
|
|
24
26
|
kcp-harness integrate --list List supported agents
|
|
27
|
+
kcp-harness export [options] Export compliance evidence
|
|
28
|
+
kcp-harness dashboard [options] Launch compliance dashboard
|
|
25
29
|
kcp-harness --version Show version
|
|
26
30
|
kcp-harness --help Show this help
|
|
27
31
|
|
|
@@ -32,7 +36,7 @@ governance for any agent. It intercepts tool calls, classifies them
|
|
|
32
36
|
as knowledge-navigation or pass-through, and routes governed calls
|
|
33
37
|
through the kcp-agent planner before execution.
|
|
34
38
|
`;
|
|
35
|
-
const VERSION = "0.
|
|
39
|
+
const VERSION = "0.4.0";
|
|
36
40
|
const TEMPLATE = `# kcp-harness configuration
|
|
37
41
|
version: "1.0"
|
|
38
42
|
|
|
@@ -57,6 +61,9 @@ governance:
|
|
|
57
61
|
# currency: USDC
|
|
58
62
|
# context_budget: 50000
|
|
59
63
|
# env: prod
|
|
64
|
+
# signature_required: true
|
|
65
|
+
# trusted_keys:
|
|
66
|
+
# - "./keys/manifest-key.pem"
|
|
60
67
|
|
|
61
68
|
downstream:
|
|
62
69
|
# - name: "filesystem"
|
|
@@ -161,6 +168,63 @@ async function main() {
|
|
|
161
168
|
}
|
|
162
169
|
break;
|
|
163
170
|
}
|
|
171
|
+
case "export": {
|
|
172
|
+
const config = existsSync(configPath) ? loadConfig(configPath) : null;
|
|
173
|
+
const auditPath = getFlag(args, "--audit") ?? config?.audit.path ?? ".kcp-harness/audit.jsonl";
|
|
174
|
+
const outDir = getFlag(args, "--out") ?? "evidence";
|
|
175
|
+
const format = (getFlag(args, "--format") ?? "both");
|
|
176
|
+
const org = getFlag(args, "--org");
|
|
177
|
+
const from = getFlag(args, "--from");
|
|
178
|
+
const to = getFlag(args, "--to");
|
|
179
|
+
if (!existsSync(auditPath)) {
|
|
180
|
+
process.stderr.write(`[kcp-harness] audit log not found: ${auditPath}\n`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const result = await exportEvidence({
|
|
185
|
+
auditPath,
|
|
186
|
+
outputDir: outDir,
|
|
187
|
+
format,
|
|
188
|
+
organization: org,
|
|
189
|
+
dateRange: from || to ? { from: from ?? "", to: to ?? "" } : undefined,
|
|
190
|
+
});
|
|
191
|
+
process.stderr.write(`[kcp-harness] exported ${result.files.length} files to ${result.outputDir}\n`);
|
|
192
|
+
process.stderr.write(`[kcp-harness] ${result.summary.events} events, ${result.summary.sessions} sessions\n`);
|
|
193
|
+
for (const f of result.files) {
|
|
194
|
+
process.stdout.write(` ${f}\n`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
199
|
+
process.stderr.write(`[kcp-harness] export error: ${msg}\n`);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case "dashboard": {
|
|
205
|
+
const config = existsSync(configPath) ? loadConfig(configPath) : null;
|
|
206
|
+
const auditPath = getFlag(args, "--audit") ?? config?.audit.path ?? ".kcp-harness/audit.jsonl";
|
|
207
|
+
const port = Number(getFlag(args, "--port") ?? "3847");
|
|
208
|
+
const host = getFlag(args, "--host") ?? "127.0.0.1";
|
|
209
|
+
if (!existsSync(auditPath)) {
|
|
210
|
+
process.stderr.write(`[kcp-harness] audit log not found: ${auditPath}\n`);
|
|
211
|
+
process.stderr.write(`[kcp-harness] the dashboard reads from the audit log — run the proxy first\n`);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
const dashboard = new DashboardServer({ auditPath, port, host });
|
|
215
|
+
await dashboard.start();
|
|
216
|
+
process.stderr.write(`[kcp-harness] dashboard running at ${dashboard.getAddress()}\n`);
|
|
217
|
+
process.stderr.write(`[kcp-harness] watching ${auditPath} for live updates\n`);
|
|
218
|
+
// Keep running until SIGINT/SIGTERM
|
|
219
|
+
const shutdown = async () => {
|
|
220
|
+
process.stderr.write(`\n[kcp-harness] shutting down dashboard\n`);
|
|
221
|
+
await dashboard.stop();
|
|
222
|
+
process.exit(0);
|
|
223
|
+
};
|
|
224
|
+
process.on("SIGINT", shutdown);
|
|
225
|
+
process.on("SIGTERM", shutdown);
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
164
228
|
default:
|
|
165
229
|
process.stderr.write(`[kcp-harness] unknown command: ${command}\n\n`);
|
|
166
230
|
process.stdout.write(USAGE);
|
package/dist/config.d.ts
CHANGED
|
@@ -28,6 +28,10 @@ export interface GovernancePolicy {
|
|
|
28
28
|
strict?: boolean;
|
|
29
29
|
/** Environment for federation context selection (dev/test/staging/prod). */
|
|
30
30
|
env?: string;
|
|
31
|
+
/** Require verified signatures on all manifests (default: false). */
|
|
32
|
+
signature_required?: boolean;
|
|
33
|
+
/** Trusted public keys for signature verification (paths, URLs, or inline). */
|
|
34
|
+
trusted_keys?: string[];
|
|
31
35
|
}
|
|
32
36
|
/** A downstream MCP server to proxy tool calls to. */
|
|
33
37
|
export interface DownstreamConfig {
|
package/dist/config.js
CHANGED
|
@@ -60,6 +60,8 @@ function parsePolicy(raw) {
|
|
|
60
60
|
max_units: p["max_units"] === undefined ? DEFAULT_POLICY.max_units : Number(p["max_units"]),
|
|
61
61
|
strict: p["strict"] === true,
|
|
62
62
|
env: p["env"] === undefined ? undefined : String(p["env"]),
|
|
63
|
+
signature_required: p["signature_required"] === true,
|
|
64
|
+
trusted_keys: Array.isArray(p["trusted_keys"]) ? p["trusted_keys"].map(String) : undefined,
|
|
63
65
|
};
|
|
64
66
|
}
|
|
65
67
|
function parseBudget(raw) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Dashboard server options. */
|
|
2
|
+
export interface DashboardOptions {
|
|
3
|
+
/** Path to the audit JSONL log. */
|
|
4
|
+
auditPath: string;
|
|
5
|
+
/** HTTP port (default: 3847). */
|
|
6
|
+
port: number;
|
|
7
|
+
/** Bind host (default: 127.0.0.1 — localhost only). */
|
|
8
|
+
host: string;
|
|
9
|
+
}
|
|
10
|
+
/** Compliance dashboard HTTP server. */
|
|
11
|
+
export declare class DashboardServer {
|
|
12
|
+
private server;
|
|
13
|
+
private tail;
|
|
14
|
+
private sseClients;
|
|
15
|
+
private readonly reader;
|
|
16
|
+
private readonly options;
|
|
17
|
+
constructor(options: DashboardOptions);
|
|
18
|
+
/** Start the dashboard server. */
|
|
19
|
+
start(): Promise<void>;
|
|
20
|
+
/** Stop the dashboard server. */
|
|
21
|
+
stop(): Promise<void>;
|
|
22
|
+
/** Get the server address. */
|
|
23
|
+
getAddress(): string;
|
|
24
|
+
private handleRequest;
|
|
25
|
+
private serveUI;
|
|
26
|
+
private serveSummary;
|
|
27
|
+
private serveSessions;
|
|
28
|
+
private serveEvents;
|
|
29
|
+
private serveSSE;
|
|
30
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Dashboard server — HTTP API + static UI over the audit log.
|
|
2
|
+
//
|
|
3
|
+
// Serves the compliance dashboard on localhost. No external dependencies
|
|
4
|
+
// — uses Node's built-in http module. Binds to 127.0.0.1 by default
|
|
5
|
+
// for security (no network exposure).
|
|
6
|
+
import { createServer } from "node:http";
|
|
7
|
+
import { AuditReader } from "../audit-reader.js";
|
|
8
|
+
import { AuditTail } from "./tail.js";
|
|
9
|
+
import { renderDashboard } from "./ui.js";
|
|
10
|
+
/** Compliance dashboard HTTP server. */
|
|
11
|
+
export class DashboardServer {
|
|
12
|
+
server = null;
|
|
13
|
+
tail = null;
|
|
14
|
+
sseClients = new Set();
|
|
15
|
+
reader;
|
|
16
|
+
options;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.options = options;
|
|
19
|
+
this.reader = new AuditReader(options.auditPath);
|
|
20
|
+
}
|
|
21
|
+
/** Start the dashboard server. */
|
|
22
|
+
async start() {
|
|
23
|
+
this.server = createServer((req, res) => this.handleRequest(req, res));
|
|
24
|
+
// Start tailing for SSE
|
|
25
|
+
this.tail = new AuditTail(this.options.auditPath);
|
|
26
|
+
this.tail.on("line", (event) => {
|
|
27
|
+
const data = `data: ${JSON.stringify(event)}\n\n`;
|
|
28
|
+
for (const client of this.sseClients) {
|
|
29
|
+
try {
|
|
30
|
+
client.write(data);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
this.sseClients.delete(client);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
this.tail.start();
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
this.server.listen(this.options.port, this.options.host, () => {
|
|
40
|
+
resolve();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** Stop the dashboard server. */
|
|
45
|
+
async stop() {
|
|
46
|
+
this.tail?.stop();
|
|
47
|
+
for (const client of this.sseClients) {
|
|
48
|
+
try {
|
|
49
|
+
client.end();
|
|
50
|
+
}
|
|
51
|
+
catch { }
|
|
52
|
+
}
|
|
53
|
+
this.sseClients.clear();
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
if (this.server) {
|
|
56
|
+
this.server.close(() => resolve());
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
resolve();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/** Get the server address. */
|
|
64
|
+
getAddress() {
|
|
65
|
+
return `http://${this.options.host}:${this.options.port}`;
|
|
66
|
+
}
|
|
67
|
+
async handleRequest(req, res) {
|
|
68
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
69
|
+
const path = url.pathname;
|
|
70
|
+
// CORS headers for localhost
|
|
71
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
72
|
+
res.setHeader("Access-Control-Allow-Methods", "GET");
|
|
73
|
+
try {
|
|
74
|
+
switch (path) {
|
|
75
|
+
case "/":
|
|
76
|
+
return this.serveUI(res);
|
|
77
|
+
case "/api/summary":
|
|
78
|
+
return await this.serveSummary(url, res);
|
|
79
|
+
case "/api/sessions":
|
|
80
|
+
return await this.serveSessions(res);
|
|
81
|
+
case "/api/events":
|
|
82
|
+
return await this.serveEvents(url, res);
|
|
83
|
+
case "/api/events/stream":
|
|
84
|
+
return this.serveSSE(req, res);
|
|
85
|
+
default:
|
|
86
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
87
|
+
res.end(JSON.stringify({ error: "not found" }));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
92
|
+
res.end(JSON.stringify({ error: e instanceof Error ? e.message : String(e) }));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
serveUI(res) {
|
|
96
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
97
|
+
res.end(renderDashboard());
|
|
98
|
+
}
|
|
99
|
+
async serveSummary(url, res) {
|
|
100
|
+
const filter = parseFilterParams(url);
|
|
101
|
+
const summary = await this.reader.summarize(filter);
|
|
102
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
103
|
+
res.end(JSON.stringify(summary));
|
|
104
|
+
}
|
|
105
|
+
async serveSessions(res) {
|
|
106
|
+
const index = await this.reader.sessionIndex();
|
|
107
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
108
|
+
res.end(JSON.stringify(index));
|
|
109
|
+
}
|
|
110
|
+
async serveEvents(url, res) {
|
|
111
|
+
const filter = parseFilterParams(url);
|
|
112
|
+
const events = await this.reader.readAll(filter);
|
|
113
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
114
|
+
res.end(JSON.stringify(events));
|
|
115
|
+
}
|
|
116
|
+
serveSSE(req, res) {
|
|
117
|
+
res.writeHead(200, {
|
|
118
|
+
"Content-Type": "text/event-stream",
|
|
119
|
+
"Cache-Control": "no-cache",
|
|
120
|
+
"Connection": "keep-alive",
|
|
121
|
+
});
|
|
122
|
+
res.write("retry: 5000\n\n");
|
|
123
|
+
this.sseClients.add(res);
|
|
124
|
+
req.on("close", () => {
|
|
125
|
+
this.sseClients.delete(res);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** Parse filter parameters from URL query string. */
|
|
130
|
+
function parseFilterParams(url) {
|
|
131
|
+
const session = url.searchParams.get("session") ?? undefined;
|
|
132
|
+
const type = url.searchParams.get("type") ?? undefined;
|
|
133
|
+
const from = url.searchParams.get("from") ?? undefined;
|
|
134
|
+
const to = url.searchParams.get("to") ?? undefined;
|
|
135
|
+
const outcome = url.searchParams.get("outcome") ?? undefined;
|
|
136
|
+
if (!session && !type && !from && !to && !outcome)
|
|
137
|
+
return undefined;
|
|
138
|
+
return {
|
|
139
|
+
sessionId: session,
|
|
140
|
+
type: type,
|
|
141
|
+
from,
|
|
142
|
+
to,
|
|
143
|
+
outcome: outcome,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AuditEvent } from "../audit.js";
|
|
2
|
+
type TailHandler = (event: AuditEvent) => void;
|
|
3
|
+
/** Tails an append-only JSONL audit log, emitting new events. */
|
|
4
|
+
export declare class AuditTail {
|
|
5
|
+
private readonly path;
|
|
6
|
+
private handlers;
|
|
7
|
+
private offset;
|
|
8
|
+
private running;
|
|
9
|
+
constructor(path: string);
|
|
10
|
+
/** Register a handler for new events. */
|
|
11
|
+
on(_event: "line", handler: TailHandler): void;
|
|
12
|
+
/** Start tailing from the current end of file. */
|
|
13
|
+
start(): void;
|
|
14
|
+
/** Stop tailing. */
|
|
15
|
+
stop(): void;
|
|
16
|
+
private readNew;
|
|
17
|
+
}
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Audit log tail — watch for new events appended to the JSONL log.
|
|
2
|
+
//
|
|
3
|
+
// Uses fs.watch + readline to tail the audit log and emit parsed events.
|
|
4
|
+
// Powers the SSE live-update endpoint in the dashboard.
|
|
5
|
+
import { createReadStream, watchFile, unwatchFile, statSync } from "node:fs";
|
|
6
|
+
import { createInterface } from "node:readline";
|
|
7
|
+
/** Tails an append-only JSONL audit log, emitting new events. */
|
|
8
|
+
export class AuditTail {
|
|
9
|
+
path;
|
|
10
|
+
handlers = [];
|
|
11
|
+
offset = 0;
|
|
12
|
+
running = false;
|
|
13
|
+
constructor(path) {
|
|
14
|
+
this.path = path;
|
|
15
|
+
}
|
|
16
|
+
/** Register a handler for new events. */
|
|
17
|
+
on(_event, handler) {
|
|
18
|
+
this.handlers.push(handler);
|
|
19
|
+
}
|
|
20
|
+
/** Start tailing from the current end of file. */
|
|
21
|
+
start() {
|
|
22
|
+
if (this.running)
|
|
23
|
+
return;
|
|
24
|
+
this.running = true;
|
|
25
|
+
try {
|
|
26
|
+
this.offset = statSync(this.path).size;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
this.offset = 0;
|
|
30
|
+
}
|
|
31
|
+
watchFile(this.path, { interval: 1000 }, () => {
|
|
32
|
+
this.readNew();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/** Stop tailing. */
|
|
36
|
+
stop() {
|
|
37
|
+
this.running = false;
|
|
38
|
+
unwatchFile(this.path);
|
|
39
|
+
}
|
|
40
|
+
readNew() {
|
|
41
|
+
if (!this.running)
|
|
42
|
+
return;
|
|
43
|
+
let currentSize;
|
|
44
|
+
try {
|
|
45
|
+
currentSize = statSync(this.path).size;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (currentSize <= this.offset)
|
|
51
|
+
return;
|
|
52
|
+
const stream = createReadStream(this.path, {
|
|
53
|
+
encoding: "utf-8",
|
|
54
|
+
start: this.offset,
|
|
55
|
+
});
|
|
56
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
57
|
+
let newOffset = this.offset;
|
|
58
|
+
rl.on("line", (line) => {
|
|
59
|
+
newOffset += Buffer.byteLength(line, "utf-8") + 1; // +1 for newline
|
|
60
|
+
const trimmed = line.trim();
|
|
61
|
+
if (!trimmed)
|
|
62
|
+
return;
|
|
63
|
+
try {
|
|
64
|
+
const event = JSON.parse(trimmed);
|
|
65
|
+
for (const handler of this.handlers) {
|
|
66
|
+
handler(event);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Skip malformed lines
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
rl.on("close", () => {
|
|
74
|
+
this.offset = newOffset;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function renderDashboard(): string;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Dashboard UI — single-page HTML/CSS/JS served as a template string.
|
|
2
|
+
//
|
|
3
|
+
// No build step, no framework, no external dependencies. The dashboard
|
|
4
|
+
// fetches data from the API endpoints and renders it in the browser.
|
|
5
|
+
export function renderDashboard() {
|
|
6
|
+
return `<!DOCTYPE html>
|
|
7
|
+
<html lang="en">
|
|
8
|
+
<head>
|
|
9
|
+
<meta charset="utf-8">
|
|
10
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
11
|
+
<title>KCP Harness — Compliance Dashboard</title>
|
|
12
|
+
<style>
|
|
13
|
+
:root {
|
|
14
|
+
--bg: #0f1117; --surface: #1a1d27; --border: #2a2d3a;
|
|
15
|
+
--text: #e1e4ed; --muted: #8b8fa3; --accent: #e94560;
|
|
16
|
+
--green: #4ade80; --red: #f87171; --yellow: #fbbf24; --blue: #60a5fa;
|
|
17
|
+
}
|
|
18
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
19
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg); color: var(--text); }
|
|
20
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
|
21
|
+
header { display: flex; align-items: center; gap: 16px; margin-bottom: 24px; border-bottom: 1px solid var(--border); padding-bottom: 16px; }
|
|
22
|
+
header h1 { font-size: 20px; font-weight: 600; }
|
|
23
|
+
header .badge { font-size: 11px; background: var(--accent); color: white; padding: 2px 8px; border-radius: 10px; }
|
|
24
|
+
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-bottom: 24px; }
|
|
25
|
+
.stat { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 16px; }
|
|
26
|
+
.stat .label { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
|
|
27
|
+
.stat .value { font-size: 28px; font-weight: 700; margin-top: 4px; }
|
|
28
|
+
.stat .value.green { color: var(--green); }
|
|
29
|
+
.stat .value.red { color: var(--red); }
|
|
30
|
+
.stat .value.yellow { color: var(--yellow); }
|
|
31
|
+
.stat .value.blue { color: var(--blue); }
|
|
32
|
+
.panel { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; margin-bottom: 16px; }
|
|
33
|
+
.panel-header { padding: 12px 16px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
|
|
34
|
+
.panel-header h2 { font-size: 14px; font-weight: 600; }
|
|
35
|
+
.panel-body { padding: 0; max-height: 500px; overflow-y: auto; }
|
|
36
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
37
|
+
th { text-align: left; padding: 8px 16px; color: var(--muted); font-weight: 500; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; position: sticky; top: 0; background: var(--surface); }
|
|
38
|
+
td { padding: 8px 16px; border-top: 1px solid var(--border); }
|
|
39
|
+
.outcome { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
|
|
40
|
+
.outcome-approved { background: rgba(74,222,128,0.15); color: var(--green); }
|
|
41
|
+
.outcome-blocked { background: rgba(248,113,113,0.15); color: var(--red); }
|
|
42
|
+
.outcome-pass-through { background: rgba(96,165,250,0.15); color: var(--blue); }
|
|
43
|
+
.outcome-error { background: rgba(251,191,36,0.15); color: var(--yellow); }
|
|
44
|
+
.type-badge { font-size: 11px; color: var(--muted); }
|
|
45
|
+
.live-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); display: inline-block; animation: pulse 2s infinite; }
|
|
46
|
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
|
|
47
|
+
.filters { display: flex; gap: 8px; align-items: center; }
|
|
48
|
+
.filters select, .filters input { background: var(--bg); border: 1px solid var(--border); color: var(--text); padding: 4px 8px; border-radius: 4px; font-size: 12px; }
|
|
49
|
+
.sessions-list { list-style: none; }
|
|
50
|
+
.sessions-list li { padding: 10px 16px; border-top: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
|
|
51
|
+
.sessions-list li:first-child { border-top: none; }
|
|
52
|
+
.session-id { font-family: monospace; font-size: 12px; color: var(--blue); cursor: pointer; }
|
|
53
|
+
.session-meta { font-size: 11px; color: var(--muted); }
|
|
54
|
+
footer { text-align: center; color: var(--muted); font-size: 11px; margin-top: 24px; padding: 16px; }
|
|
55
|
+
</style>
|
|
56
|
+
</head>
|
|
57
|
+
<body>
|
|
58
|
+
<div class="container">
|
|
59
|
+
<header>
|
|
60
|
+
<h1>KCP Harness</h1>
|
|
61
|
+
<span class="badge">Compliance Dashboard</span>
|
|
62
|
+
<span style="margin-left:auto;font-size:12px;color:var(--muted)"><span class="live-dot"></span> Live</span>
|
|
63
|
+
</header>
|
|
64
|
+
|
|
65
|
+
<div class="stats" id="stats">
|
|
66
|
+
<div class="stat"><div class="label">Sessions</div><div class="value blue" id="s-sessions">-</div></div>
|
|
67
|
+
<div class="stat"><div class="label">Events</div><div class="value" id="s-events">-</div></div>
|
|
68
|
+
<div class="stat"><div class="label">Governed</div><div class="value green" id="s-governed">-</div></div>
|
|
69
|
+
<div class="stat"><div class="label">Blocked</div><div class="value red" id="s-blocked">-</div></div>
|
|
70
|
+
<div class="stat"><div class="label">Budget Exceeded</div><div class="value yellow" id="s-budget">-</div></div>
|
|
71
|
+
<div class="stat"><div class="label">Drifts</div><div class="value yellow" id="s-drifts">-</div></div>
|
|
72
|
+
<div class="stat"><div class="label">Sig. Blocked</div><div class="value red" id="s-sig">-</div></div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div class="panel">
|
|
76
|
+
<div class="panel-header">
|
|
77
|
+
<h2>Sessions</h2>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="panel-body">
|
|
80
|
+
<ul class="sessions-list" id="sessions-list"></ul>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div class="panel">
|
|
85
|
+
<div class="panel-header">
|
|
86
|
+
<h2>Event Log</h2>
|
|
87
|
+
<div class="filters">
|
|
88
|
+
<select id="f-type"><option value="">All types</option>
|
|
89
|
+
<option value="tool_call">tool_call</option>
|
|
90
|
+
<option value="session_start">session_start</option>
|
|
91
|
+
<option value="session_end">session_end</option>
|
|
92
|
+
<option value="budget_spend">budget_spend</option>
|
|
93
|
+
<option value="budget_exceeded">budget_exceeded</option>
|
|
94
|
+
<option value="temporal_drift">temporal_drift</option>
|
|
95
|
+
</select>
|
|
96
|
+
<select id="f-outcome"><option value="">All outcomes</option>
|
|
97
|
+
<option value="approved">approved</option>
|
|
98
|
+
<option value="blocked">blocked</option>
|
|
99
|
+
<option value="pass-through">pass-through</option>
|
|
100
|
+
<option value="error">error</option>
|
|
101
|
+
</select>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="panel-body">
|
|
105
|
+
<table>
|
|
106
|
+
<thead><tr><th>Time</th><th>Session</th><th>Type</th><th>Tool</th><th>Outcome</th><th>Reason</th></tr></thead>
|
|
107
|
+
<tbody id="events-body"></tbody>
|
|
108
|
+
</table>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<footer>kcp-harness compliance dashboard · all data local · no external connections</footer>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<script>
|
|
116
|
+
const API = '';
|
|
117
|
+
let allEvents = [];
|
|
118
|
+
|
|
119
|
+
async function load() {
|
|
120
|
+
const [summary, sessions, events] = await Promise.all([
|
|
121
|
+
fetch(API + '/api/summary').then(r => r.json()),
|
|
122
|
+
fetch(API + '/api/sessions').then(r => r.json()),
|
|
123
|
+
fetch(API + '/api/events').then(r => r.json()),
|
|
124
|
+
]);
|
|
125
|
+
updateStats(summary);
|
|
126
|
+
renderSessions(sessions.sessions || []);
|
|
127
|
+
allEvents = events;
|
|
128
|
+
renderEvents(events);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function updateStats(s) {
|
|
132
|
+
document.getElementById('s-sessions').textContent = s.sessions ?? 0;
|
|
133
|
+
document.getElementById('s-events').textContent = s.events ?? 0;
|
|
134
|
+
document.getElementById('s-governed').textContent = s.governed ?? 0;
|
|
135
|
+
document.getElementById('s-blocked').textContent = s.blocked ?? 0;
|
|
136
|
+
document.getElementById('s-budget').textContent = s.budgetExceeded ?? 0;
|
|
137
|
+
document.getElementById('s-drifts').textContent = s.drifts ?? 0;
|
|
138
|
+
document.getElementById('s-sig').textContent = s.signatureBlocked ?? 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function renderSessions(sessions) {
|
|
142
|
+
const ul = document.getElementById('sessions-list');
|
|
143
|
+
if (!sessions.length) { ul.innerHTML = '<li style="color:var(--muted)">No sessions yet</li>'; return; }
|
|
144
|
+
ul.innerHTML = sessions.map(s =>
|
|
145
|
+
'<li><div><span class="session-id" onclick="filterSession(\\'' + s.id + '\\')">' + s.id.slice(0,12) + '</span>' +
|
|
146
|
+
'<div class="session-meta">' + s.events + ' events, ' + s.governed + ' governed, ' + s.blocked + ' blocked</div></div>' +
|
|
147
|
+
'<div class="session-meta">' + new Date(s.startedAt).toLocaleString() + '</div></li>'
|
|
148
|
+
).join('');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function renderEvents(events) {
|
|
152
|
+
const type = document.getElementById('f-type').value;
|
|
153
|
+
const outcome = document.getElementById('f-outcome').value;
|
|
154
|
+
let filtered = events;
|
|
155
|
+
if (type) filtered = filtered.filter(e => e.type === type);
|
|
156
|
+
if (outcome) filtered = filtered.filter(e => e.outcome === outcome);
|
|
157
|
+
const last200 = filtered.slice(-200);
|
|
158
|
+
const tbody = document.getElementById('events-body');
|
|
159
|
+
tbody.innerHTML = last200.map(e =>
|
|
160
|
+
'<tr><td style="font-size:11px;white-space:nowrap">' + new Date(e.timestamp).toLocaleTimeString() + '</td>' +
|
|
161
|
+
'<td style="font-family:monospace;font-size:11px">' + (e.sessionId||'').slice(0,8) + '</td>' +
|
|
162
|
+
'<td><span class="type-badge">' + e.type + '</span></td>' +
|
|
163
|
+
'<td>' + (e.toolCall?.name || '-') + '</td>' +
|
|
164
|
+
'<td><span class="outcome outcome-' + e.outcome + '">' + e.outcome + '</span></td>' +
|
|
165
|
+
'<td style="font-size:11px;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + (e.governance?.reason || e.error || e.type) + '</td></tr>'
|
|
166
|
+
).join('');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function filterSession(id) {
|
|
170
|
+
const filtered = allEvents.filter(e => e.sessionId === id);
|
|
171
|
+
renderEvents(filtered);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
document.getElementById('f-type').onchange = () => renderEvents(allEvents);
|
|
175
|
+
document.getElementById('f-outcome').onchange = () => renderEvents(allEvents);
|
|
176
|
+
|
|
177
|
+
// SSE live updates
|
|
178
|
+
const sse = new EventSource(API + '/api/events/stream');
|
|
179
|
+
sse.onmessage = (msg) => {
|
|
180
|
+
try {
|
|
181
|
+
const event = JSON.parse(msg.data);
|
|
182
|
+
allEvents.push(event);
|
|
183
|
+
renderEvents(allEvents);
|
|
184
|
+
fetch(API + '/api/summary').then(r => r.json()).then(updateStats);
|
|
185
|
+
} catch {}
|
|
186
|
+
};
|
|
187
|
+
sse.onerror = () => {};
|
|
188
|
+
|
|
189
|
+
load();
|
|
190
|
+
</script>
|
|
191
|
+
</body>
|
|
192
|
+
</html>`;
|
|
193
|
+
}
|
package/dist/export.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type AuditSummary } from "./audit-reader.js";
|
|
2
|
+
/** Export configuration. */
|
|
3
|
+
export interface ExportOptions {
|
|
4
|
+
/** Path to the audit JSONL log. */
|
|
5
|
+
auditPath: string;
|
|
6
|
+
/** Output directory for evidence bundles. */
|
|
7
|
+
outputDir: string;
|
|
8
|
+
/** Which framework(s) to export. */
|
|
9
|
+
format: "soc2" | "iso27001" | "both";
|
|
10
|
+
/** Optional date range filter. */
|
|
11
|
+
dateRange?: {
|
|
12
|
+
from: string;
|
|
13
|
+
to: string;
|
|
14
|
+
};
|
|
15
|
+
/** Organization name for the report header. */
|
|
16
|
+
organization?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Result of an export operation. */
|
|
19
|
+
export interface ExportResult {
|
|
20
|
+
/** Output directory. */
|
|
21
|
+
outputDir: string;
|
|
22
|
+
/** Files created. */
|
|
23
|
+
files: string[];
|
|
24
|
+
/** Summary statistics. */
|
|
25
|
+
summary: AuditSummary;
|
|
26
|
+
}
|
|
27
|
+
/** Export compliance evidence from the audit log. */
|
|
28
|
+
export declare function exportEvidence(options: ExportOptions): Promise<ExportResult>;
|