speclock 1.7.0 → 2.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.
- package/README.md +28 -13
- package/package.json +9 -3
- package/src/cli/index.js +82 -5
- package/src/core/audit.js +237 -0
- package/src/core/compliance.js +291 -0
- package/src/core/engine.js +48 -68
- package/src/core/license.js +221 -0
- package/src/core/llm-checker.js +239 -0
- package/src/core/semantics.js +1096 -0
- package/src/core/storage.js +9 -0
- package/src/mcp/http-server.js +120 -5
- package/src/mcp/server.js +78 -2
package/src/core/storage.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import crypto from "crypto";
|
|
4
|
+
import { signEvent, isAuditEnabled } from "./audit.js";
|
|
4
5
|
|
|
5
6
|
export function nowIso() {
|
|
6
7
|
return new Date().toISOString();
|
|
@@ -140,6 +141,14 @@ export function writeBrain(root, brain) {
|
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
export function appendEvent(root, event) {
|
|
144
|
+
// HMAC audit chain — sign event if audit is enabled
|
|
145
|
+
try {
|
|
146
|
+
if (isAuditEnabled(root)) {
|
|
147
|
+
signEvent(root, event);
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
// Audit error — write event without hash (graceful degradation)
|
|
151
|
+
}
|
|
143
152
|
const line = JSON.stringify(event);
|
|
144
153
|
fs.appendFileSync(eventsPath(root), `${line}\n`);
|
|
145
154
|
}
|
package/src/mcp/http-server.js
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
updateDeployFacts,
|
|
20
20
|
logChange,
|
|
21
21
|
checkConflict,
|
|
22
|
+
checkConflictAsync,
|
|
22
23
|
getSessionBriefing,
|
|
23
24
|
endSession,
|
|
24
25
|
suggestLocks,
|
|
@@ -27,6 +28,8 @@ import {
|
|
|
27
28
|
applyTemplate,
|
|
28
29
|
generateReport,
|
|
29
30
|
auditStagedFiles,
|
|
31
|
+
verifyAuditChain,
|
|
32
|
+
exportCompliance,
|
|
30
33
|
} from "../core/engine.js";
|
|
31
34
|
import { generateContext, generateContextPack } from "../core/context.js";
|
|
32
35
|
import {
|
|
@@ -45,8 +48,51 @@ import {
|
|
|
45
48
|
} from "../core/git.js";
|
|
46
49
|
|
|
47
50
|
const PROJECT_ROOT = process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
|
|
48
|
-
const VERSION = "1.
|
|
51
|
+
const VERSION = "2.1.0";
|
|
49
52
|
const AUTHOR = "Sandeep Roy";
|
|
53
|
+
const START_TIME = Date.now();
|
|
54
|
+
|
|
55
|
+
// --- Rate Limiting ---
|
|
56
|
+
const RATE_LIMIT = parseInt(process.env.SPECLOCK_RATE_LIMIT || "100", 10);
|
|
57
|
+
const RATE_WINDOW_MS = 60_000;
|
|
58
|
+
const rateLimitMap = new Map();
|
|
59
|
+
|
|
60
|
+
function checkRateLimit(ip) {
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
if (!rateLimitMap.has(ip)) {
|
|
63
|
+
rateLimitMap.set(ip, []);
|
|
64
|
+
}
|
|
65
|
+
const timestamps = rateLimitMap.get(ip).filter((t) => now - t < RATE_WINDOW_MS);
|
|
66
|
+
timestamps.push(now);
|
|
67
|
+
rateLimitMap.set(ip, timestamps);
|
|
68
|
+
return timestamps.length <= RATE_LIMIT;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Clean up stale entries every 5 minutes
|
|
72
|
+
setInterval(() => {
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
for (const [ip, timestamps] of rateLimitMap) {
|
|
75
|
+
const active = timestamps.filter((t) => now - t < RATE_WINDOW_MS);
|
|
76
|
+
if (active.length === 0) rateLimitMap.delete(ip);
|
|
77
|
+
else rateLimitMap.set(ip, active);
|
|
78
|
+
}
|
|
79
|
+
}, 5 * 60_000);
|
|
80
|
+
|
|
81
|
+
// --- CORS Configuration ---
|
|
82
|
+
const ALLOWED_ORIGINS = process.env.SPECLOCK_CORS_ORIGINS
|
|
83
|
+
? process.env.SPECLOCK_CORS_ORIGINS.split(",").map((s) => s.trim())
|
|
84
|
+
: ["*"];
|
|
85
|
+
|
|
86
|
+
function setCorsHeaders(res) {
|
|
87
|
+
const origin = ALLOWED_ORIGINS.includes("*") ? "*" : ALLOWED_ORIGINS.join(", ");
|
|
88
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
89
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
90
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
91
|
+
res.setHeader("Access-Control-Max-Age", "86400");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- Request Size Limit ---
|
|
95
|
+
const MAX_BODY_SIZE = 1024 * 1024; // 1MB
|
|
50
96
|
|
|
51
97
|
function createSpecLockServer() {
|
|
52
98
|
const server = new McpServer(
|
|
@@ -176,7 +222,7 @@ function createSpecLockServer() {
|
|
|
176
222
|
// Tool 12: speclock_check_conflict
|
|
177
223
|
server.tool("speclock_check_conflict", "Check if a proposed action conflicts with any active SpecLock.", { proposedAction: z.string().min(1).describe("Description of the action") }, async ({ proposedAction }) => {
|
|
178
224
|
ensureInit(PROJECT_ROOT);
|
|
179
|
-
const result =
|
|
225
|
+
const result = await checkConflictAsync(PROJECT_ROOT, proposedAction);
|
|
180
226
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
181
227
|
});
|
|
182
228
|
|
|
@@ -292,13 +338,57 @@ function createSpecLockServer() {
|
|
|
292
338
|
return { content: [{ type: "text", text: `## Audit Failed\n\n${text}\n\n${result.message}` }] };
|
|
293
339
|
});
|
|
294
340
|
|
|
341
|
+
// Tool 23: speclock_verify_audit
|
|
342
|
+
server.tool("speclock_verify_audit", "Verify the integrity of the HMAC audit chain.", {}, async () => {
|
|
343
|
+
ensureInit(PROJECT_ROOT);
|
|
344
|
+
const result = verifyAuditChain(PROJECT_ROOT);
|
|
345
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Tool 24: speclock_export_compliance
|
|
349
|
+
server.tool("speclock_export_compliance", "Generate compliance reports (SOC 2, HIPAA, CSV).", { format: z.enum(["soc2", "hipaa", "csv"]).describe("Export format") }, async ({ format }) => {
|
|
350
|
+
ensureInit(PROJECT_ROOT);
|
|
351
|
+
const result = exportCompliance(PROJECT_ROOT, format);
|
|
352
|
+
if (result.error) return { content: [{ type: "text", text: result.error }], isError: true };
|
|
353
|
+
const output = format === "csv" ? result.data : JSON.stringify(result.data, null, 2);
|
|
354
|
+
return { content: [{ type: "text", text: output }] };
|
|
355
|
+
});
|
|
356
|
+
|
|
295
357
|
return server;
|
|
296
358
|
}
|
|
297
359
|
|
|
298
360
|
// --- HTTP Server ---
|
|
299
361
|
const app = createMcpExpressApp({ host: "0.0.0.0" });
|
|
300
362
|
|
|
363
|
+
// CORS preflight handler
|
|
364
|
+
app.options("*", (req, res) => {
|
|
365
|
+
setCorsHeaders(res);
|
|
366
|
+
res.writeHead(204).end();
|
|
367
|
+
});
|
|
368
|
+
|
|
301
369
|
app.post("/mcp", async (req, res) => {
|
|
370
|
+
setCorsHeaders(res);
|
|
371
|
+
|
|
372
|
+
// Rate limiting
|
|
373
|
+
const clientIp = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket?.remoteAddress || "unknown";
|
|
374
|
+
if (!checkRateLimit(clientIp)) {
|
|
375
|
+
return res.status(429).json({
|
|
376
|
+
jsonrpc: "2.0",
|
|
377
|
+
error: { code: -32000, message: `Rate limit exceeded (${RATE_LIMIT} req/min). Try again later.` },
|
|
378
|
+
id: null,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Request size check
|
|
383
|
+
const contentLength = parseInt(req.headers["content-length"] || "0", 10);
|
|
384
|
+
if (contentLength > MAX_BODY_SIZE) {
|
|
385
|
+
return res.status(413).json({
|
|
386
|
+
jsonrpc: "2.0",
|
|
387
|
+
error: { code: -32000, message: `Request too large (max ${MAX_BODY_SIZE / 1024}KB)` },
|
|
388
|
+
id: null,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
302
392
|
const server = createSpecLockServer();
|
|
303
393
|
try {
|
|
304
394
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
@@ -317,22 +407,47 @@ app.post("/mcp", async (req, res) => {
|
|
|
317
407
|
});
|
|
318
408
|
|
|
319
409
|
app.get("/mcp", async (req, res) => {
|
|
410
|
+
setCorsHeaders(res);
|
|
320
411
|
res.writeHead(405).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed." }, id: null }));
|
|
321
412
|
});
|
|
322
413
|
|
|
323
414
|
app.delete("/mcp", async (req, res) => {
|
|
415
|
+
setCorsHeaders(res);
|
|
324
416
|
res.writeHead(405).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed." }, id: null }));
|
|
325
417
|
});
|
|
326
418
|
|
|
327
|
-
// Health check endpoint
|
|
419
|
+
// Health check endpoint (enhanced for enterprise)
|
|
420
|
+
app.get("/health", (req, res) => {
|
|
421
|
+
setCorsHeaders(res);
|
|
422
|
+
let auditStatus = "unknown";
|
|
423
|
+
try {
|
|
424
|
+
const result = verifyAuditChain(PROJECT_ROOT);
|
|
425
|
+
auditStatus = result.valid ? "valid" : "broken";
|
|
426
|
+
} catch {
|
|
427
|
+
auditStatus = "unavailable";
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
res.json({
|
|
431
|
+
status: "healthy",
|
|
432
|
+
version: VERSION,
|
|
433
|
+
uptime: Math.floor((Date.now() - START_TIME) / 1000),
|
|
434
|
+
tools: 24,
|
|
435
|
+
auditChain: auditStatus,
|
|
436
|
+
rateLimit: { limit: RATE_LIMIT, windowMs: RATE_WINDOW_MS },
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Root info endpoint
|
|
328
441
|
app.get("/", (req, res) => {
|
|
442
|
+
setCorsHeaders(res);
|
|
329
443
|
res.json({
|
|
330
444
|
name: "speclock",
|
|
331
445
|
version: VERSION,
|
|
332
446
|
author: AUTHOR,
|
|
333
|
-
description: "AI Continuity Engine
|
|
334
|
-
tools:
|
|
447
|
+
description: "AI Continuity Engine with enterprise audit, compliance, and enforcement",
|
|
448
|
+
tools: 24,
|
|
335
449
|
mcp_endpoint: "/mcp",
|
|
450
|
+
health_endpoint: "/health",
|
|
336
451
|
npm: "https://www.npmjs.com/package/speclock",
|
|
337
452
|
github: "https://github.com/sgroy10/speclock",
|
|
338
453
|
});
|
package/src/mcp/server.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
updateDeployFacts,
|
|
13
13
|
logChange,
|
|
14
14
|
checkConflict,
|
|
15
|
+
checkConflictAsync,
|
|
15
16
|
getSessionBriefing,
|
|
16
17
|
endSession,
|
|
17
18
|
suggestLocks,
|
|
@@ -22,6 +23,10 @@ import {
|
|
|
22
23
|
applyTemplate,
|
|
23
24
|
generateReport,
|
|
24
25
|
auditStagedFiles,
|
|
26
|
+
verifyAuditChain,
|
|
27
|
+
exportCompliance,
|
|
28
|
+
checkLimits,
|
|
29
|
+
getLicenseInfo,
|
|
25
30
|
} from "../core/engine.js";
|
|
26
31
|
import { generateContext, generateContextPack } from "../core/context.js";
|
|
27
32
|
import {
|
|
@@ -56,7 +61,7 @@ const PROJECT_ROOT =
|
|
|
56
61
|
args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
|
|
57
62
|
|
|
58
63
|
// --- MCP Server ---
|
|
59
|
-
const VERSION = "1.
|
|
64
|
+
const VERSION = "2.1.0";
|
|
60
65
|
const AUTHOR = "Sandeep Roy";
|
|
61
66
|
|
|
62
67
|
const server = new McpServer(
|
|
@@ -427,7 +432,7 @@ server.tool(
|
|
|
427
432
|
.describe("Description of the action you plan to take"),
|
|
428
433
|
},
|
|
429
434
|
async ({ proposedAction }) => {
|
|
430
|
-
const result =
|
|
435
|
+
const result = await checkConflictAsync(PROJECT_ROOT, proposedAction);
|
|
431
436
|
return {
|
|
432
437
|
content: [{ type: "text", text: result.analysis }],
|
|
433
438
|
};
|
|
@@ -882,6 +887,77 @@ server.tool(
|
|
|
882
887
|
}
|
|
883
888
|
);
|
|
884
889
|
|
|
890
|
+
// ========================================
|
|
891
|
+
// ENTERPRISE TOOLS (v2.1)
|
|
892
|
+
// ========================================
|
|
893
|
+
|
|
894
|
+
// Tool 23: speclock_verify_audit
|
|
895
|
+
server.tool(
|
|
896
|
+
"speclock_verify_audit",
|
|
897
|
+
"Verify the integrity of the HMAC audit chain. Detects tampering or corruption in the event log. Returns chain status, total events, and any broken links.",
|
|
898
|
+
{},
|
|
899
|
+
async () => {
|
|
900
|
+
ensureInit(PROJECT_ROOT);
|
|
901
|
+
const result = verifyAuditChain(PROJECT_ROOT);
|
|
902
|
+
|
|
903
|
+
const status = result.valid ? "VALID" : "BROKEN";
|
|
904
|
+
const parts = [
|
|
905
|
+
`## Audit Chain Verification`,
|
|
906
|
+
``,
|
|
907
|
+
`Status: **${status}**`,
|
|
908
|
+
`Total events: ${result.totalEvents}`,
|
|
909
|
+
`Hashed events: ${result.hashedEvents}`,
|
|
910
|
+
`Legacy events (pre-v2.1): ${result.unhashedEvents}`,
|
|
911
|
+
];
|
|
912
|
+
|
|
913
|
+
if (!result.valid && result.errors) {
|
|
914
|
+
parts.push(``, `### Errors`);
|
|
915
|
+
for (const err of result.errors) {
|
|
916
|
+
parts.push(`- Line ${err.line}: ${err.error}${err.eventId ? ` (${err.eventId})` : ""}`);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
parts.push(``, result.message);
|
|
921
|
+
parts.push(``, `Verified at: ${result.verifiedAt}`);
|
|
922
|
+
|
|
923
|
+
return {
|
|
924
|
+
content: [{ type: "text", text: parts.join("\n") }],
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
);
|
|
928
|
+
|
|
929
|
+
// Tool 24: speclock_export_compliance
|
|
930
|
+
server.tool(
|
|
931
|
+
"speclock_export_compliance",
|
|
932
|
+
"Generate compliance reports for enterprise auditing. Supports SOC 2 Type II, HIPAA, and CSV formats. Reports include constraint management, access logs, audit chain integrity, and violation history.",
|
|
933
|
+
{
|
|
934
|
+
format: z
|
|
935
|
+
.enum(["soc2", "hipaa", "csv"])
|
|
936
|
+
.describe("Export format: soc2 (JSON), hipaa (JSON), csv (spreadsheet)"),
|
|
937
|
+
},
|
|
938
|
+
async ({ format }) => {
|
|
939
|
+
ensureInit(PROJECT_ROOT);
|
|
940
|
+
const result = exportCompliance(PROJECT_ROOT, format);
|
|
941
|
+
|
|
942
|
+
if (result.error) {
|
|
943
|
+
return {
|
|
944
|
+
content: [{ type: "text", text: result.error }],
|
|
945
|
+
isError: true,
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
if (format === "csv") {
|
|
950
|
+
return {
|
|
951
|
+
content: [{ type: "text", text: `## Compliance Export (CSV)\n\n\`\`\`csv\n${result.data}\n\`\`\`` }],
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
return {
|
|
956
|
+
content: [{ type: "text", text: `## Compliance Export (${format.toUpperCase()})\n\n\`\`\`json\n${JSON.stringify(result.data, null, 2)}\n\`\`\`` }],
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
);
|
|
960
|
+
|
|
885
961
|
// --- Smithery sandbox export ---
|
|
886
962
|
export default function createSandboxServer() {
|
|
887
963
|
return server;
|