speclock 2.1.1 → 3.0.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 +55 -5
- package/package.json +10 -4
- package/src/cli/index.js +247 -3
- package/src/core/auth.js +341 -0
- package/src/core/compliance.js +1 -1
- package/src/core/conflict.js +363 -0
- package/src/core/crypto.js +158 -0
- package/src/core/enforcer.js +314 -0
- package/src/core/engine.js +111 -781
- package/src/core/memory.js +191 -0
- package/src/core/pre-commit-semantic.js +284 -0
- package/src/core/sessions.js +128 -0
- package/src/core/storage.js +23 -4
- package/src/core/tracking.js +98 -0
- package/src/mcp/http-server.js +134 -7
- package/src/mcp/server.js +206 -4
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpecLock Tracking Module
|
|
3
|
+
* Change logging, file event handling, event management.
|
|
4
|
+
* Extracted from engine.js for modularity.
|
|
5
|
+
*
|
|
6
|
+
* Developed by Sandeep Roy (https://github.com/sgroy10)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import {
|
|
12
|
+
nowIso,
|
|
13
|
+
newId,
|
|
14
|
+
writeBrain,
|
|
15
|
+
appendEvent,
|
|
16
|
+
bumpEvents,
|
|
17
|
+
speclockDir,
|
|
18
|
+
addRecentChange,
|
|
19
|
+
addRevert,
|
|
20
|
+
} from "./storage.js";
|
|
21
|
+
import { captureDiff } from "./git.js";
|
|
22
|
+
import { ensureInit } from "./memory.js";
|
|
23
|
+
|
|
24
|
+
// --- Internal helpers ---
|
|
25
|
+
|
|
26
|
+
function recordEvent(root, brain, event) {
|
|
27
|
+
bumpEvents(brain, event.eventId);
|
|
28
|
+
appendEvent(root, event);
|
|
29
|
+
writeBrain(root, brain);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writePatch(root, eventId, content) {
|
|
33
|
+
const patchPath = path.join(
|
|
34
|
+
speclockDir(root),
|
|
35
|
+
"patches",
|
|
36
|
+
`${eventId}.patch`
|
|
37
|
+
);
|
|
38
|
+
fs.writeFileSync(patchPath, content);
|
|
39
|
+
return path.join(".speclock", "patches", `${eventId}.patch`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// --- Core functions ---
|
|
43
|
+
|
|
44
|
+
export function logChange(root, summary, files = []) {
|
|
45
|
+
const brain = ensureInit(root);
|
|
46
|
+
const eventId = newId("evt");
|
|
47
|
+
let patchPath = "";
|
|
48
|
+
if (brain.facts.repo.hasGit) {
|
|
49
|
+
const diff = captureDiff(root);
|
|
50
|
+
if (diff && diff.trim().length > 0) {
|
|
51
|
+
patchPath = writePatch(root, eventId, diff);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const event = {
|
|
55
|
+
eventId,
|
|
56
|
+
type: "manual_change",
|
|
57
|
+
at: nowIso(),
|
|
58
|
+
files,
|
|
59
|
+
summary,
|
|
60
|
+
patchPath,
|
|
61
|
+
};
|
|
62
|
+
addRecentChange(brain, {
|
|
63
|
+
eventId,
|
|
64
|
+
summary,
|
|
65
|
+
files,
|
|
66
|
+
at: event.at,
|
|
67
|
+
});
|
|
68
|
+
recordEvent(root, brain, event);
|
|
69
|
+
return { brain, eventId };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function handleFileEvent(root, brain, type, filePath) {
|
|
73
|
+
const eventId = newId("evt");
|
|
74
|
+
const rel = path.relative(root, filePath);
|
|
75
|
+
let patchPath = "";
|
|
76
|
+
if (brain.facts.repo.hasGit) {
|
|
77
|
+
const diff = captureDiff(root);
|
|
78
|
+
const patchContent =
|
|
79
|
+
diff && diff.trim().length > 0 ? diff : "(no diff available)";
|
|
80
|
+
patchPath = writePatch(root, eventId, patchContent);
|
|
81
|
+
}
|
|
82
|
+
const summary = `${type.replace("_", " ")}: ${rel}`;
|
|
83
|
+
const event = {
|
|
84
|
+
eventId,
|
|
85
|
+
type,
|
|
86
|
+
at: nowIso(),
|
|
87
|
+
files: [rel],
|
|
88
|
+
summary,
|
|
89
|
+
patchPath,
|
|
90
|
+
};
|
|
91
|
+
addRecentChange(brain, {
|
|
92
|
+
eventId,
|
|
93
|
+
summary,
|
|
94
|
+
files: [rel],
|
|
95
|
+
at: event.at,
|
|
96
|
+
});
|
|
97
|
+
recordEvent(root, brain, event);
|
|
98
|
+
}
|
package/src/mcp/http-server.js
CHANGED
|
@@ -30,6 +30,11 @@ import {
|
|
|
30
30
|
auditStagedFiles,
|
|
31
31
|
verifyAuditChain,
|
|
32
32
|
exportCompliance,
|
|
33
|
+
enforceConflictCheck,
|
|
34
|
+
setEnforcementMode,
|
|
35
|
+
overrideLock,
|
|
36
|
+
getOverrideHistory,
|
|
37
|
+
semanticAudit,
|
|
33
38
|
} from "../core/engine.js";
|
|
34
39
|
import { generateContext, generateContextPack } from "../core/context.js";
|
|
35
40
|
import {
|
|
@@ -46,9 +51,21 @@ import {
|
|
|
46
51
|
createTag,
|
|
47
52
|
getDiffSummary,
|
|
48
53
|
} from "../core/git.js";
|
|
54
|
+
import {
|
|
55
|
+
isAuthEnabled,
|
|
56
|
+
validateApiKey,
|
|
57
|
+
checkPermission,
|
|
58
|
+
createApiKey,
|
|
59
|
+
rotateApiKey,
|
|
60
|
+
revokeApiKey,
|
|
61
|
+
listApiKeys,
|
|
62
|
+
enableAuth,
|
|
63
|
+
disableAuth,
|
|
64
|
+
TOOL_PERMISSIONS,
|
|
65
|
+
} from "../core/auth.js";
|
|
49
66
|
|
|
50
67
|
const PROJECT_ROOT = process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
|
|
51
|
-
const VERSION = "
|
|
68
|
+
const VERSION = "3.0.0";
|
|
52
69
|
const AUTHOR = "Sandeep Roy";
|
|
53
70
|
const START_TIME = Date.now();
|
|
54
71
|
|
|
@@ -219,11 +236,14 @@ function createSpecLockServer() {
|
|
|
219
236
|
return { content: [{ type: "text", text: events.length ? JSON.stringify(events, null, 2) : "No matching events." }] };
|
|
220
237
|
});
|
|
221
238
|
|
|
222
|
-
// Tool 12: speclock_check_conflict
|
|
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 }) => {
|
|
239
|
+
// Tool 12: speclock_check_conflict (v2.5: uses enforcer)
|
|
240
|
+
server.tool("speclock_check_conflict", "Check if a proposed action conflicts with any active SpecLock. In hard mode, blocks above threshold.", { proposedAction: z.string().min(1).describe("Description of the action") }, async ({ proposedAction }) => {
|
|
224
241
|
ensureInit(PROJECT_ROOT);
|
|
225
|
-
const result =
|
|
226
|
-
|
|
242
|
+
const result = enforceConflictCheck(PROJECT_ROOT, proposedAction);
|
|
243
|
+
if (result.blocked) {
|
|
244
|
+
return { content: [{ type: "text", text: result.analysis }], isError: true };
|
|
245
|
+
}
|
|
246
|
+
return { content: [{ type: "text", text: result.analysis }] };
|
|
227
247
|
});
|
|
228
248
|
|
|
229
249
|
// Tool 13: speclock_session_briefing
|
|
@@ -354,6 +374,35 @@ function createSpecLockServer() {
|
|
|
354
374
|
return { content: [{ type: "text", text: output }] };
|
|
355
375
|
});
|
|
356
376
|
|
|
377
|
+
// Tool 25: speclock_set_enforcement (v2.5)
|
|
378
|
+
server.tool("speclock_set_enforcement", "Set enforcement mode: advisory (warn) or hard (block).", { mode: z.enum(["advisory", "hard"]).describe("Enforcement mode"), blockThreshold: z.number().int().min(0).max(100).optional().default(70).describe("Block threshold %") }, async ({ mode, blockThreshold }) => {
|
|
379
|
+
const result = setEnforcementMode(PROJECT_ROOT, mode, { blockThreshold });
|
|
380
|
+
if (!result.success) return { content: [{ type: "text", text: result.error }], isError: true };
|
|
381
|
+
return { content: [{ type: "text", text: `Enforcement: ${mode} (threshold: ${result.config.blockThreshold}%)` }] };
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Tool 26: speclock_override_lock (v2.5)
|
|
385
|
+
server.tool("speclock_override_lock", "Override a lock with justification. Logged to audit trail.", { lockId: z.string().min(1), action: z.string().min(1), reason: z.string().min(1) }, async ({ lockId, action, reason }) => {
|
|
386
|
+
const result = overrideLock(PROJECT_ROOT, lockId, action, reason);
|
|
387
|
+
if (!result.success) return { content: [{ type: "text", text: result.error }], isError: true };
|
|
388
|
+
const msg = result.escalated ? `\n${result.escalationMessage}` : "";
|
|
389
|
+
return { content: [{ type: "text", text: `Override: "${result.lockText}" (${result.overrideCount}x)${msg}` }] };
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// Tool 27: speclock_semantic_audit (v2.5)
|
|
393
|
+
server.tool("speclock_semantic_audit", "Semantic pre-commit: analyzes code changes vs locks.", {}, async () => {
|
|
394
|
+
const result = semanticAudit(PROJECT_ROOT);
|
|
395
|
+
return { content: [{ type: "text", text: result.message }], isError: result.blocked || false };
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Tool 28: speclock_override_history (v2.5)
|
|
399
|
+
server.tool("speclock_override_history", "Show lock override history.", { lockId: z.string().optional() }, async ({ lockId }) => {
|
|
400
|
+
const result = getOverrideHistory(PROJECT_ROOT, lockId);
|
|
401
|
+
if (result.total === 0) return { content: [{ type: "text", text: "No overrides recorded." }] };
|
|
402
|
+
const lines = result.overrides.map(o => `[${o.at.substring(0,19)}] "${o.lockText}" — ${o.reason}`).join("\n");
|
|
403
|
+
return { content: [{ type: "text", text: `Overrides (${result.total}):\n${lines}` }] };
|
|
404
|
+
});
|
|
405
|
+
|
|
357
406
|
return server;
|
|
358
407
|
}
|
|
359
408
|
|
|
@@ -366,6 +415,16 @@ app.options("*", (req, res) => {
|
|
|
366
415
|
res.writeHead(204).end();
|
|
367
416
|
});
|
|
368
417
|
|
|
418
|
+
// --- Auth middleware helper ---
|
|
419
|
+
function authenticateRequest(req) {
|
|
420
|
+
if (!isAuthEnabled(PROJECT_ROOT)) {
|
|
421
|
+
return { valid: true, role: "admin", authEnabled: false };
|
|
422
|
+
}
|
|
423
|
+
const authHeader = req.headers["authorization"] || "";
|
|
424
|
+
const rawKey = authHeader.startsWith("Bearer ") ? authHeader.slice(7).trim() : "";
|
|
425
|
+
return validateApiKey(PROJECT_ROOT, rawKey);
|
|
426
|
+
}
|
|
427
|
+
|
|
369
428
|
app.post("/mcp", async (req, res) => {
|
|
370
429
|
setCorsHeaders(res);
|
|
371
430
|
|
|
@@ -389,6 +448,28 @@ app.post("/mcp", async (req, res) => {
|
|
|
389
448
|
});
|
|
390
449
|
}
|
|
391
450
|
|
|
451
|
+
// Authentication (v3.0)
|
|
452
|
+
const auth = authenticateRequest(req);
|
|
453
|
+
if (!auth.valid) {
|
|
454
|
+
return res.status(401).json({
|
|
455
|
+
jsonrpc: "2.0",
|
|
456
|
+
error: { code: -32000, message: auth.error || "Authentication required." },
|
|
457
|
+
id: null,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// RBAC check — extract tool name from JSON-RPC body for permission check
|
|
462
|
+
if (auth.authEnabled && req.body && req.body.method === "tools/call") {
|
|
463
|
+
const toolName = req.body.params?.name;
|
|
464
|
+
if (toolName && !checkPermission(auth.role, toolName)) {
|
|
465
|
+
return res.status(403).json({
|
|
466
|
+
jsonrpc: "2.0",
|
|
467
|
+
error: { code: -32000, message: `Permission denied. Role "${auth.role}" cannot access "${toolName}". Required: ${TOOL_PERMISSIONS[toolName] || "admin"}` },
|
|
468
|
+
id: req.body.id || null,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
392
473
|
const server = createSpecLockServer();
|
|
393
474
|
try {
|
|
394
475
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
@@ -406,6 +487,51 @@ app.post("/mcp", async (req, res) => {
|
|
|
406
487
|
}
|
|
407
488
|
});
|
|
408
489
|
|
|
490
|
+
// --- Auth management endpoint (v3.0) ---
|
|
491
|
+
app.post("/auth", async (req, res) => {
|
|
492
|
+
setCorsHeaders(res);
|
|
493
|
+
const auth = authenticateRequest(req);
|
|
494
|
+
|
|
495
|
+
const { action } = req.body || {};
|
|
496
|
+
|
|
497
|
+
// Creating the first key doesn't require auth (bootstrap)
|
|
498
|
+
if (action === "create-key" && !isAuthEnabled(PROJECT_ROOT)) {
|
|
499
|
+
const { role, name } = req.body;
|
|
500
|
+
const result = createApiKey(PROJECT_ROOT, role || "admin", name || "");
|
|
501
|
+
return res.json(result);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// All other auth actions require admin role
|
|
505
|
+
if (auth.authEnabled && (!auth.valid || auth.role !== "admin")) {
|
|
506
|
+
return res.status(auth.valid ? 403 : 401).json({
|
|
507
|
+
error: auth.valid ? "Admin role required for auth management." : auth.error,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
switch (action) {
|
|
512
|
+
case "create-key": {
|
|
513
|
+
const { role, name } = req.body;
|
|
514
|
+
return res.json(createApiKey(PROJECT_ROOT, role || "developer", name || ""));
|
|
515
|
+
}
|
|
516
|
+
case "rotate-key": {
|
|
517
|
+
const { keyId } = req.body;
|
|
518
|
+
return res.json(rotateApiKey(PROJECT_ROOT, keyId));
|
|
519
|
+
}
|
|
520
|
+
case "revoke-key": {
|
|
521
|
+
const { keyId, reason } = req.body;
|
|
522
|
+
return res.json(revokeApiKey(PROJECT_ROOT, keyId, reason || "manual"));
|
|
523
|
+
}
|
|
524
|
+
case "list-keys":
|
|
525
|
+
return res.json(listApiKeys(PROJECT_ROOT));
|
|
526
|
+
case "enable":
|
|
527
|
+
return res.json(enableAuth(PROJECT_ROOT));
|
|
528
|
+
case "disable":
|
|
529
|
+
return res.json(disableAuth(PROJECT_ROOT));
|
|
530
|
+
default:
|
|
531
|
+
return res.status(400).json({ error: `Unknown auth action: "${action}". Valid: create-key, rotate-key, revoke-key, list-keys, enable, disable` });
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
|
|
409
535
|
app.get("/mcp", async (req, res) => {
|
|
410
536
|
setCorsHeaders(res);
|
|
411
537
|
res.writeHead(405).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed." }, id: null }));
|
|
@@ -431,8 +557,9 @@ app.get("/health", (req, res) => {
|
|
|
431
557
|
status: "healthy",
|
|
432
558
|
version: VERSION,
|
|
433
559
|
uptime: Math.floor((Date.now() - START_TIME) / 1000),
|
|
434
|
-
tools:
|
|
560
|
+
tools: 28,
|
|
435
561
|
auditChain: auditStatus,
|
|
562
|
+
authEnabled: isAuthEnabled(PROJECT_ROOT),
|
|
436
563
|
rateLimit: { limit: RATE_LIMIT, windowMs: RATE_WINDOW_MS },
|
|
437
564
|
});
|
|
438
565
|
});
|
|
@@ -445,7 +572,7 @@ app.get("/", (req, res) => {
|
|
|
445
572
|
version: VERSION,
|
|
446
573
|
author: AUTHOR,
|
|
447
574
|
description: "AI Continuity Engine with enterprise audit, compliance, and enforcement",
|
|
448
|
-
tools:
|
|
575
|
+
tools: 28,
|
|
449
576
|
mcp_endpoint: "/mcp",
|
|
450
577
|
health_endpoint: "/health",
|
|
451
578
|
npm: "https://www.npmjs.com/package/speclock",
|
package/src/mcp/server.js
CHANGED
|
@@ -27,6 +27,12 @@ import {
|
|
|
27
27
|
exportCompliance,
|
|
28
28
|
checkLimits,
|
|
29
29
|
getLicenseInfo,
|
|
30
|
+
enforceConflictCheck,
|
|
31
|
+
setEnforcementMode,
|
|
32
|
+
overrideLock,
|
|
33
|
+
getOverrideHistory,
|
|
34
|
+
getEnforcementConfig,
|
|
35
|
+
semanticAudit,
|
|
30
36
|
} from "../core/engine.js";
|
|
31
37
|
import { generateContext, generateContextPack } from "../core/context.js";
|
|
32
38
|
import {
|
|
@@ -43,6 +49,29 @@ import {
|
|
|
43
49
|
createTag,
|
|
44
50
|
getDiffSummary,
|
|
45
51
|
} from "../core/git.js";
|
|
52
|
+
import {
|
|
53
|
+
isAuthEnabled,
|
|
54
|
+
validateApiKey,
|
|
55
|
+
checkPermission,
|
|
56
|
+
} from "../core/auth.js";
|
|
57
|
+
|
|
58
|
+
// --- Auth via env var (v3.0) ---
|
|
59
|
+
function getAuthRole() {
|
|
60
|
+
if (!isAuthEnabled(PROJECT_ROOT)) return "admin";
|
|
61
|
+
const key = process.env.SPECLOCK_API_KEY;
|
|
62
|
+
if (!key) return "admin"; // No key env var = local use, allow all
|
|
63
|
+
const result = validateApiKey(PROJECT_ROOT, key);
|
|
64
|
+
return result.valid ? result.role : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function requirePermission(toolName) {
|
|
68
|
+
const role = getAuthRole();
|
|
69
|
+
if (!role) return { allowed: false, error: "Invalid SPECLOCK_API_KEY." };
|
|
70
|
+
if (!checkPermission(role, toolName)) {
|
|
71
|
+
return { allowed: false, error: `Permission denied. Role "${role}" cannot access "${toolName}".` };
|
|
72
|
+
}
|
|
73
|
+
return { allowed: true, role };
|
|
74
|
+
}
|
|
46
75
|
|
|
47
76
|
// --- Project root resolution ---
|
|
48
77
|
function parseArgs(argv) {
|
|
@@ -61,7 +90,7 @@ const PROJECT_ROOT =
|
|
|
61
90
|
args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
|
|
62
91
|
|
|
63
92
|
// --- MCP Server ---
|
|
64
|
-
const VERSION = "
|
|
93
|
+
const VERSION = "3.0.0";
|
|
65
94
|
const AUTHOR = "Sandeep Roy";
|
|
66
95
|
|
|
67
96
|
const server = new McpServer(
|
|
@@ -421,10 +450,10 @@ server.tool(
|
|
|
421
450
|
// CONTINUITY PROTECTION TOOLS
|
|
422
451
|
// ========================================
|
|
423
452
|
|
|
424
|
-
// Tool 12: speclock_check_conflict
|
|
453
|
+
// Tool 12: speclock_check_conflict (v2.5: uses enforcer — hard mode returns isError)
|
|
425
454
|
server.tool(
|
|
426
455
|
"speclock_check_conflict",
|
|
427
|
-
"Check if a proposed action conflicts with any active SpecLock. Use before making significant changes.",
|
|
456
|
+
"Check if a proposed action conflicts with any active SpecLock. Use before making significant changes. In hard enforcement mode, conflicts above the threshold will BLOCK the action (isError: true).",
|
|
428
457
|
{
|
|
429
458
|
proposedAction: z
|
|
430
459
|
.string()
|
|
@@ -432,7 +461,28 @@ server.tool(
|
|
|
432
461
|
.describe("Description of the action you plan to take"),
|
|
433
462
|
},
|
|
434
463
|
async ({ proposedAction }) => {
|
|
435
|
-
|
|
464
|
+
// Try LLM-enhanced check first, fall back to heuristic enforcer
|
|
465
|
+
let result;
|
|
466
|
+
try {
|
|
467
|
+
const { llmCheckConflict } = await import("../core/llm-checker.js");
|
|
468
|
+
const llmResult = await llmCheckConflict(PROJECT_ROOT, proposedAction);
|
|
469
|
+
if (llmResult) {
|
|
470
|
+
result = llmResult;
|
|
471
|
+
}
|
|
472
|
+
} catch (_) {}
|
|
473
|
+
|
|
474
|
+
if (!result) {
|
|
475
|
+
result = enforceConflictCheck(PROJECT_ROOT, proposedAction);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// In hard mode with blocking conflict, return isError: true
|
|
479
|
+
if (result.blocked) {
|
|
480
|
+
return {
|
|
481
|
+
content: [{ type: "text", text: result.analysis }],
|
|
482
|
+
isError: true,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
436
486
|
return {
|
|
437
487
|
content: [{ type: "text", text: result.analysis }],
|
|
438
488
|
};
|
|
@@ -958,6 +1008,158 @@ server.tool(
|
|
|
958
1008
|
}
|
|
959
1009
|
);
|
|
960
1010
|
|
|
1011
|
+
// ========================================
|
|
1012
|
+
// HARD ENFORCEMENT TOOLS (v2.5)
|
|
1013
|
+
// ========================================
|
|
1014
|
+
|
|
1015
|
+
// Tool 25: speclock_set_enforcement
|
|
1016
|
+
server.tool(
|
|
1017
|
+
"speclock_set_enforcement",
|
|
1018
|
+
"Set the enforcement mode for this project. 'advisory' (default) warns about conflicts. 'hard' blocks actions that violate locks above the confidence threshold — the AI cannot proceed.",
|
|
1019
|
+
{
|
|
1020
|
+
mode: z
|
|
1021
|
+
.enum(["advisory", "hard"])
|
|
1022
|
+
.describe("Enforcement mode: advisory (warn) or hard (block)"),
|
|
1023
|
+
blockThreshold: z
|
|
1024
|
+
.number()
|
|
1025
|
+
.int()
|
|
1026
|
+
.min(0)
|
|
1027
|
+
.max(100)
|
|
1028
|
+
.optional()
|
|
1029
|
+
.default(70)
|
|
1030
|
+
.describe("Minimum confidence % to block in hard mode (default: 70)"),
|
|
1031
|
+
allowOverride: z
|
|
1032
|
+
.boolean()
|
|
1033
|
+
.optional()
|
|
1034
|
+
.default(true)
|
|
1035
|
+
.describe("Whether lock overrides are permitted"),
|
|
1036
|
+
},
|
|
1037
|
+
async ({ mode, blockThreshold, allowOverride }) => {
|
|
1038
|
+
const result = setEnforcementMode(PROJECT_ROOT, mode, { blockThreshold, allowOverride });
|
|
1039
|
+
if (!result.success) {
|
|
1040
|
+
return {
|
|
1041
|
+
content: [{ type: "text", text: result.error }],
|
|
1042
|
+
isError: true,
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
return {
|
|
1046
|
+
content: [
|
|
1047
|
+
{
|
|
1048
|
+
type: "text",
|
|
1049
|
+
text: `Enforcement mode set to **${mode}**. Threshold: ${result.config.blockThreshold}%. Overrides: ${result.config.allowOverride ? "allowed" : "disabled"}.`,
|
|
1050
|
+
},
|
|
1051
|
+
],
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
);
|
|
1055
|
+
|
|
1056
|
+
// Tool 26: speclock_override_lock
|
|
1057
|
+
server.tool(
|
|
1058
|
+
"speclock_override_lock",
|
|
1059
|
+
"Override a lock for a specific action. Requires a reason which is logged to the audit trail. Use when a locked action must proceed with justification. Triggers escalation after repeated overrides.",
|
|
1060
|
+
{
|
|
1061
|
+
lockId: z.string().min(1).describe("The lock ID to override"),
|
|
1062
|
+
action: z.string().min(1).describe("The action that conflicts with the lock"),
|
|
1063
|
+
reason: z.string().min(1).describe("Justification for the override"),
|
|
1064
|
+
},
|
|
1065
|
+
async ({ lockId, action, reason }) => {
|
|
1066
|
+
const result = overrideLock(PROJECT_ROOT, lockId, action, reason);
|
|
1067
|
+
if (!result.success) {
|
|
1068
|
+
return {
|
|
1069
|
+
content: [{ type: "text", text: result.error }],
|
|
1070
|
+
isError: true,
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const parts = [
|
|
1075
|
+
`Lock overridden: "${result.lockText}"`,
|
|
1076
|
+
`Override count: ${result.overrideCount}`,
|
|
1077
|
+
`Reason: ${reason}`,
|
|
1078
|
+
];
|
|
1079
|
+
|
|
1080
|
+
if (result.escalated) {
|
|
1081
|
+
parts.push("", result.escalationMessage);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
return {
|
|
1085
|
+
content: [{ type: "text", text: parts.join("\n") }],
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
);
|
|
1089
|
+
|
|
1090
|
+
// Tool 27: speclock_semantic_audit
|
|
1091
|
+
server.tool(
|
|
1092
|
+
"speclock_semantic_audit",
|
|
1093
|
+
"Run semantic pre-commit audit. Parses the staged git diff, analyzes actual code changes against active locks using semantic analysis. Much more powerful than filename-only audit — catches violations in code content.",
|
|
1094
|
+
{},
|
|
1095
|
+
async () => {
|
|
1096
|
+
const result = semanticAudit(PROJECT_ROOT);
|
|
1097
|
+
|
|
1098
|
+
if (result.passed) {
|
|
1099
|
+
return {
|
|
1100
|
+
content: [{ type: "text", text: result.message }],
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const formatted = result.violations
|
|
1105
|
+
.map((v) => {
|
|
1106
|
+
const lines = [`- [${v.level}] **${v.file}** (confidence: ${v.confidence}%)`];
|
|
1107
|
+
lines.push(` Lock: "${v.lockText}"`);
|
|
1108
|
+
lines.push(` Reason: ${v.reason}`);
|
|
1109
|
+
if (v.addedLines) lines.push(` Changes: +${v.addedLines} / -${v.removedLines} lines`);
|
|
1110
|
+
return lines.join("\n");
|
|
1111
|
+
})
|
|
1112
|
+
.join("\n\n");
|
|
1113
|
+
|
|
1114
|
+
return {
|
|
1115
|
+
content: [
|
|
1116
|
+
{
|
|
1117
|
+
type: "text",
|
|
1118
|
+
text: `## Semantic Audit Result\n\nMode: ${result.mode} | Threshold: ${result.threshold}%\n\n${formatted}\n\n${result.message}`,
|
|
1119
|
+
},
|
|
1120
|
+
],
|
|
1121
|
+
isError: result.blocked || false,
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
);
|
|
1125
|
+
|
|
1126
|
+
// Tool 28: speclock_override_history
|
|
1127
|
+
server.tool(
|
|
1128
|
+
"speclock_override_history",
|
|
1129
|
+
"Get the history of lock overrides. Shows which locks have been overridden, by whom, and the reasons given. Useful for audit review and identifying locks that may need updating.",
|
|
1130
|
+
{
|
|
1131
|
+
lockId: z
|
|
1132
|
+
.string()
|
|
1133
|
+
.optional()
|
|
1134
|
+
.describe("Filter by specific lock ID. Omit to see all overrides."),
|
|
1135
|
+
},
|
|
1136
|
+
async ({ lockId }) => {
|
|
1137
|
+
const result = getOverrideHistory(PROJECT_ROOT, lockId);
|
|
1138
|
+
|
|
1139
|
+
if (result.total === 0) {
|
|
1140
|
+
return {
|
|
1141
|
+
content: [{ type: "text", text: "No overrides recorded." }],
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const formatted = result.overrides
|
|
1146
|
+
.map(
|
|
1147
|
+
(o) =>
|
|
1148
|
+
`- [${o.at.substring(0, 19)}] Lock: "${o.lockText}" (${o.lockId})\n Action: ${o.action}\n Reason: ${o.reason}`
|
|
1149
|
+
)
|
|
1150
|
+
.join("\n\n");
|
|
1151
|
+
|
|
1152
|
+
return {
|
|
1153
|
+
content: [
|
|
1154
|
+
{
|
|
1155
|
+
type: "text",
|
|
1156
|
+
text: `## Override History (${result.total})\n\n${formatted}`,
|
|
1157
|
+
},
|
|
1158
|
+
],
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
);
|
|
1162
|
+
|
|
961
1163
|
// --- Smithery sandbox export ---
|
|
962
1164
|
export default function createSandboxServer() {
|
|
963
1165
|
return server;
|