speclock 4.5.7 → 5.0.1
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 +145 -9
- package/package.json +67 -131
- package/src/cli/index.js +1 -1
- package/src/core/code-graph.js +635 -0
- package/src/core/compliance.js +1 -1
- package/src/core/conflict.js +1 -0
- package/src/core/engine.js +32 -2
- package/src/core/llm-checker.js +3 -156
- package/src/core/llm-provider.js +208 -0
- package/src/core/memory.js +115 -0
- package/src/core/spec-compiler.js +315 -0
- package/src/core/typed-constraints.js +408 -0
- package/src/dashboard/index.html +5 -4
- package/src/mcp/http-server.js +596 -7
- package/src/mcp/server.js +383 -1
package/src/mcp/http-server.js
CHANGED
|
@@ -36,6 +36,22 @@ import {
|
|
|
36
36
|
overrideLock,
|
|
37
37
|
getOverrideHistory,
|
|
38
38
|
semanticAudit,
|
|
39
|
+
addTypedLock,
|
|
40
|
+
updateTypedLockThreshold,
|
|
41
|
+
checkAllTypedConstraints,
|
|
42
|
+
getEnforcementConfig,
|
|
43
|
+
CONSTRAINT_TYPES,
|
|
44
|
+
OPERATORS,
|
|
45
|
+
checkTypedConstraint,
|
|
46
|
+
formatTypedLockText,
|
|
47
|
+
compileSpec,
|
|
48
|
+
compileAndApply,
|
|
49
|
+
buildGraph,
|
|
50
|
+
getOrBuildGraph,
|
|
51
|
+
getBlastRadius,
|
|
52
|
+
mapLocksToFiles,
|
|
53
|
+
getModules,
|
|
54
|
+
getCriticalPaths,
|
|
39
55
|
} from "../core/engine.js";
|
|
40
56
|
import { generateContext, generateContextPack } from "../core/context.js";
|
|
41
57
|
import {
|
|
@@ -91,7 +107,7 @@ import { fileURLToPath } from "url";
|
|
|
91
107
|
import _path from "path";
|
|
92
108
|
|
|
93
109
|
const PROJECT_ROOT = process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
|
|
94
|
-
const VERSION = "
|
|
110
|
+
const VERSION = "5.0.0";
|
|
95
111
|
const AUTHOR = "Sandeep Roy";
|
|
96
112
|
const START_TIME = Date.now();
|
|
97
113
|
|
|
@@ -512,6 +528,101 @@ function createSpecLockServer() {
|
|
|
512
528
|
return { content: [{ type: "text", text: `Overrides (${result.total}):\n${lines}` }] };
|
|
513
529
|
});
|
|
514
530
|
|
|
531
|
+
// ========================================
|
|
532
|
+
// TYPED CONSTRAINTS — Autonomous Systems Governance (v5.0)
|
|
533
|
+
// ========================================
|
|
534
|
+
|
|
535
|
+
// Tool 29: speclock_add_typed_lock
|
|
536
|
+
server.tool("speclock_add_typed_lock", "Add a typed constraint for autonomous systems governance.", {
|
|
537
|
+
constraintType: z.enum(["numerical", "range", "state", "temporal"]),
|
|
538
|
+
metric: z.string().optional(),
|
|
539
|
+
operator: z.enum(["<", "<=", "==", "!=", ">=", ">"]).optional(),
|
|
540
|
+
value: z.number().optional(),
|
|
541
|
+
min: z.number().optional(),
|
|
542
|
+
max: z.number().optional(),
|
|
543
|
+
unit: z.string().optional(),
|
|
544
|
+
entity: z.string().optional(),
|
|
545
|
+
forbidden: z.array(z.object({ from: z.string(), to: z.string() })).optional(),
|
|
546
|
+
requireApproval: z.boolean().optional(),
|
|
547
|
+
description: z.string().optional(),
|
|
548
|
+
tags: z.array(z.string()).default([]),
|
|
549
|
+
source: z.enum(["user", "agent"]).default("user"),
|
|
550
|
+
}, async (params) => {
|
|
551
|
+
const constraint = {
|
|
552
|
+
constraintType: params.constraintType,
|
|
553
|
+
...(params.metric && { metric: params.metric }),
|
|
554
|
+
...(params.operator && { operator: params.operator }),
|
|
555
|
+
...(params.value !== undefined && { value: params.value }),
|
|
556
|
+
...(params.min !== undefined && { min: params.min }),
|
|
557
|
+
...(params.max !== undefined && { max: params.max }),
|
|
558
|
+
...(params.unit && { unit: params.unit }),
|
|
559
|
+
...(params.entity && { entity: params.entity }),
|
|
560
|
+
...(params.forbidden && { forbidden: params.forbidden }),
|
|
561
|
+
...(params.requireApproval !== undefined && { requireApproval: params.requireApproval }),
|
|
562
|
+
};
|
|
563
|
+
const result = addTypedLock(PROJECT_ROOT, constraint, params.tags, params.source, params.description);
|
|
564
|
+
if (result.error) return { content: [{ type: "text", text: `Error: ${result.error}` }], isError: true };
|
|
565
|
+
return { content: [{ type: "text", text: `Typed lock added (${params.constraintType}): ${result.lockId}` }] };
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// Tool 30: speclock_check_typed
|
|
569
|
+
server.tool("speclock_check_typed", "Check a proposed value or state transition against typed constraints.", {
|
|
570
|
+
metric: z.string().optional(),
|
|
571
|
+
entity: z.string().optional(),
|
|
572
|
+
value: z.number().optional(),
|
|
573
|
+
from: z.string().optional(),
|
|
574
|
+
to: z.string().optional(),
|
|
575
|
+
}, async (params) => {
|
|
576
|
+
const brain = ensureInit(PROJECT_ROOT);
|
|
577
|
+
const proposed = {
|
|
578
|
+
...(params.metric && { metric: params.metric }),
|
|
579
|
+
...(params.entity && { entity: params.entity }),
|
|
580
|
+
...(params.value !== undefined && { value: params.value }),
|
|
581
|
+
...(params.from && { from: params.from }),
|
|
582
|
+
...(params.to && { to: params.to }),
|
|
583
|
+
};
|
|
584
|
+
const result = checkAllTypedConstraints(brain.specLock?.items || [], proposed);
|
|
585
|
+
if (result.hasConflict) {
|
|
586
|
+
const enforcement = getEnforcementConfig(PROJECT_ROOT);
|
|
587
|
+
const isHard = enforcement.mode === "hard";
|
|
588
|
+
const topConf = result.conflictingLocks[0]?.confidence || 0;
|
|
589
|
+
return {
|
|
590
|
+
content: [{ type: "text", text: `VIOLATION: ${result.analysis}` }],
|
|
591
|
+
isError: isHard && topConf >= enforcement.blockThreshold,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
return { content: [{ type: "text", text: result.analysis }] };
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// Tool 31: speclock_list_typed_locks
|
|
598
|
+
server.tool("speclock_list_typed_locks", "List all typed constraints.", {}, async () => {
|
|
599
|
+
const brain = ensureInit(PROJECT_ROOT);
|
|
600
|
+
const typed = (brain.specLock?.items || []).filter(l => l.active !== false && l.constraintType);
|
|
601
|
+
if (typed.length === 0) return { content: [{ type: "text", text: "No typed constraints. Use speclock_add_typed_lock to add." }] };
|
|
602
|
+
const lines = typed.map(l => `[${l.constraintType}] ${l.id}: ${l.text}`).join("\n");
|
|
603
|
+
return { content: [{ type: "text", text: `Typed Constraints (${typed.length}):\n${lines}` }] };
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
// Tool 32: speclock_update_threshold
|
|
607
|
+
server.tool("speclock_update_threshold", "Update a typed lock threshold.", {
|
|
608
|
+
lockId: z.string().min(1),
|
|
609
|
+
value: z.number().optional(),
|
|
610
|
+
operator: z.enum(["<", "<=", "==", "!=", ">=", ">"]).optional(),
|
|
611
|
+
min: z.number().optional(),
|
|
612
|
+
max: z.number().optional(),
|
|
613
|
+
forbidden: z.array(z.object({ from: z.string(), to: z.string() })).optional(),
|
|
614
|
+
}, async (params) => {
|
|
615
|
+
const updates = {};
|
|
616
|
+
if (params.value !== undefined) updates.value = params.value;
|
|
617
|
+
if (params.operator) updates.operator = params.operator;
|
|
618
|
+
if (params.min !== undefined) updates.min = params.min;
|
|
619
|
+
if (params.max !== undefined) updates.max = params.max;
|
|
620
|
+
if (params.forbidden) updates.forbidden = params.forbidden;
|
|
621
|
+
const result = updateTypedLockThreshold(PROJECT_ROOT, params.lockId, updates);
|
|
622
|
+
if (result.error) return { content: [{ type: "text", text: `Error: ${result.error}` }], isError: true };
|
|
623
|
+
return { content: [{ type: "text", text: `Updated ${params.lockId}: ${JSON.stringify(result.newValues)}` }] };
|
|
624
|
+
});
|
|
625
|
+
|
|
515
626
|
return server;
|
|
516
627
|
}
|
|
517
628
|
|
|
@@ -770,7 +881,7 @@ app.get("/health", (req, res) => {
|
|
|
770
881
|
status: "healthy",
|
|
771
882
|
version: VERSION,
|
|
772
883
|
uptime: Math.floor((Date.now() - START_TIME) / 1000),
|
|
773
|
-
tools:
|
|
884
|
+
tools: 35,
|
|
774
885
|
auditChain: auditStatus,
|
|
775
886
|
authEnabled: isAuthEnabled(PROJECT_ROOT),
|
|
776
887
|
rateLimit: { limit: RATE_LIMIT, windowMs: RATE_WINDOW_MS },
|
|
@@ -784,8 +895,8 @@ app.get("/", (req, res) => {
|
|
|
784
895
|
name: "speclock",
|
|
785
896
|
version: VERSION,
|
|
786
897
|
author: AUTHOR,
|
|
787
|
-
description: "AI Constraint Engine with Policy-as-Code
|
|
788
|
-
tools:
|
|
898
|
+
description: "AI Constraint Engine for autonomous systems governance. Typed constraints (numerical, range, state, temporal) + REST API v2 with batch checking & SSE streaming. Python SDK + ROS2 integration. Policy-as-Code, OAuth/OIDC SSO, admin dashboard, telemetry, RBAC, AES-256-GCM encryption, hard enforcement, HMAC audit chain, SOC 2/HIPAA compliance. 35 MCP tools.",
|
|
899
|
+
tools: 35,
|
|
789
900
|
mcp_endpoint: "/mcp",
|
|
790
901
|
health_endpoint: "/health",
|
|
791
902
|
npm: "https://www.npmjs.com/package/speclock",
|
|
@@ -799,7 +910,7 @@ app.get("/.well-known/mcp/server-card.json", (req, res) => {
|
|
|
799
910
|
res.json({
|
|
800
911
|
name: "SpecLock",
|
|
801
912
|
version: VERSION,
|
|
802
|
-
description: "AI Constraint Engine
|
|
913
|
+
description: "AI Constraint Engine for autonomous systems governance. Typed constraints + REST API v2 with batch checking & SSE streaming. Python SDK (pip install speclock) + ROS2 Guardian Node. Hybrid heuristic + Gemini LLM. Policy-as-Code, OAuth/OIDC SSO, admin dashboard, telemetry, RBAC, AES-256-GCM encryption, hard enforcement, HMAC audit chain, SOC 2/HIPAA compliance. 35 MCP tools + CLI. Works with Claude Code, Cursor, Windsurf, Cline, Bolt.new, Lovable.",
|
|
803
914
|
author: {
|
|
804
915
|
name: "Sandeep Roy",
|
|
805
916
|
url: "https://github.com/sgroy10",
|
|
@@ -808,7 +919,7 @@ app.get("/.well-known/mcp/server-card.json", (req, res) => {
|
|
|
808
919
|
homepage: "https://sgroy10.github.io/speclock/",
|
|
809
920
|
license: "MIT",
|
|
810
921
|
capabilities: {
|
|
811
|
-
tools:
|
|
922
|
+
tools: 35,
|
|
812
923
|
categories: [
|
|
813
924
|
"Memory Management",
|
|
814
925
|
"Change Tracking",
|
|
@@ -820,6 +931,11 @@ app.get("/.well-known/mcp/server-card.json", (req, res) => {
|
|
|
820
931
|
"Hard Enforcement",
|
|
821
932
|
"Policy-as-Code",
|
|
822
933
|
"Telemetry",
|
|
934
|
+
"Typed Constraints",
|
|
935
|
+
"REST API v2",
|
|
936
|
+
"Real-Time Streaming",
|
|
937
|
+
"Robotics / ROS2",
|
|
938
|
+
"Python SDK",
|
|
823
939
|
],
|
|
824
940
|
},
|
|
825
941
|
keywords: [
|
|
@@ -827,6 +943,8 @@ app.get("/.well-known/mcp/server-card.json", (req, res) => {
|
|
|
827
943
|
"sso", "oauth", "rbac", "encryption", "audit", "compliance",
|
|
828
944
|
"soc2", "hipaa", "dashboard", "telemetry", "claude-code",
|
|
829
945
|
"cursor", "bolt-new", "lovable", "enterprise",
|
|
946
|
+
"robotics", "ros2", "autonomous-systems", "iot", "typed-constraints",
|
|
947
|
+
"real-time", "sse", "batch-api", "python-sdk", "safety",
|
|
830
948
|
],
|
|
831
949
|
});
|
|
832
950
|
});
|
|
@@ -922,6 +1040,475 @@ app.post("/policy", async (req, res) => {
|
|
|
922
1040
|
}
|
|
923
1041
|
});
|
|
924
1042
|
|
|
1043
|
+
// ========================================
|
|
1044
|
+
// REST API v2 — Real-Time Constraint Checking (v5.0)
|
|
1045
|
+
// For robotics, IoT, autonomous systems, trading platforms.
|
|
1046
|
+
// Sub-millisecond local checks, batch operations, SSE streaming.
|
|
1047
|
+
// Developed by Sandeep Roy (https://github.com/sgroy10)
|
|
1048
|
+
// ========================================
|
|
1049
|
+
|
|
1050
|
+
// --- v2: Check a single typed constraint ---
|
|
1051
|
+
app.post("/api/v2/check-typed", (req, res) => {
|
|
1052
|
+
setCorsHeaders(res);
|
|
1053
|
+
const clientIp = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket?.remoteAddress || "unknown";
|
|
1054
|
+
if (!checkRateLimit(clientIp)) {
|
|
1055
|
+
return res.status(429).json({ error: "Rate limit exceeded." });
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const start = performance.now();
|
|
1059
|
+
const { metric, entity, value, from_state, to_state } = req.body || {};
|
|
1060
|
+
|
|
1061
|
+
if (!metric && !entity) {
|
|
1062
|
+
return res.status(400).json({ error: "Required: metric (string) or entity (string)" });
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
try {
|
|
1066
|
+
ensureInit(PROJECT_ROOT);
|
|
1067
|
+
const brain = readBrain(PROJECT_ROOT);
|
|
1068
|
+
const locks = brain?.specLock?.items || [];
|
|
1069
|
+
|
|
1070
|
+
const proposed = {};
|
|
1071
|
+
if (metric) proposed.metric = metric;
|
|
1072
|
+
if (entity) proposed.entity = entity;
|
|
1073
|
+
if (value !== undefined) proposed.value = value;
|
|
1074
|
+
if (from_state) proposed.from = from_state;
|
|
1075
|
+
if (to_state) proposed.to = to_state;
|
|
1076
|
+
|
|
1077
|
+
const result = checkAllTypedConstraints(locks, proposed);
|
|
1078
|
+
const elapsed = performance.now() - start;
|
|
1079
|
+
|
|
1080
|
+
return res.json({
|
|
1081
|
+
...result,
|
|
1082
|
+
response_time_ms: Number(elapsed.toFixed(3)),
|
|
1083
|
+
api_version: "v2",
|
|
1084
|
+
});
|
|
1085
|
+
} catch (err) {
|
|
1086
|
+
return res.status(500).json({ error: err.message });
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
// --- v2: Batch check multiple values at once ---
|
|
1091
|
+
// Single HTTP call for all sensor readings in a tick.
|
|
1092
|
+
// Body: { checks: [{ metric, value }, { entity, from_state, to_state }, ...] }
|
|
1093
|
+
app.post("/api/v2/check-batch", (req, res) => {
|
|
1094
|
+
setCorsHeaders(res);
|
|
1095
|
+
const clientIp = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket?.remoteAddress || "unknown";
|
|
1096
|
+
if (!checkRateLimit(clientIp)) {
|
|
1097
|
+
return res.status(429).json({ error: "Rate limit exceeded." });
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const start = performance.now();
|
|
1101
|
+
const { checks } = req.body || {};
|
|
1102
|
+
|
|
1103
|
+
if (!Array.isArray(checks) || checks.length === 0) {
|
|
1104
|
+
return res.status(400).json({ error: "Required: checks (non-empty array)" });
|
|
1105
|
+
}
|
|
1106
|
+
if (checks.length > 100) {
|
|
1107
|
+
return res.status(400).json({ error: "Too many checks (max 100 per batch)" });
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
try {
|
|
1111
|
+
ensureInit(PROJECT_ROOT);
|
|
1112
|
+
const brain = readBrain(PROJECT_ROOT);
|
|
1113
|
+
const locks = brain?.specLock?.items || [];
|
|
1114
|
+
|
|
1115
|
+
const results = [];
|
|
1116
|
+
let totalViolations = 0;
|
|
1117
|
+
let criticalCount = 0;
|
|
1118
|
+
|
|
1119
|
+
for (const check of checks) {
|
|
1120
|
+
const proposed = {};
|
|
1121
|
+
if (check.metric) proposed.metric = check.metric;
|
|
1122
|
+
if (check.entity) proposed.entity = check.entity;
|
|
1123
|
+
if (check.value !== undefined) proposed.value = check.value;
|
|
1124
|
+
if (check.from_state) proposed.from = check.from_state;
|
|
1125
|
+
if (check.to_state) proposed.to = check.to_state;
|
|
1126
|
+
|
|
1127
|
+
const result = checkAllTypedConstraints(locks, proposed);
|
|
1128
|
+
if (result.hasConflict) {
|
|
1129
|
+
totalViolations++;
|
|
1130
|
+
const topConfidence = result.conflictingLocks?.[0]?.confidence || 0;
|
|
1131
|
+
if (topConfidence >= 90) criticalCount++;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
results.push({
|
|
1135
|
+
input: check,
|
|
1136
|
+
...result,
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const elapsed = performance.now() - start;
|
|
1141
|
+
|
|
1142
|
+
return res.json({
|
|
1143
|
+
batch_size: checks.length,
|
|
1144
|
+
total_violations: totalViolations,
|
|
1145
|
+
critical_violations: criticalCount,
|
|
1146
|
+
emergency_stop: criticalCount > 0,
|
|
1147
|
+
results,
|
|
1148
|
+
response_time_ms: Number(elapsed.toFixed(3)),
|
|
1149
|
+
avg_check_ms: Number((elapsed / checks.length).toFixed(3)),
|
|
1150
|
+
api_version: "v2",
|
|
1151
|
+
});
|
|
1152
|
+
} catch (err) {
|
|
1153
|
+
return res.status(500).json({ error: err.message });
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
// --- v2: Add typed constraint via REST ---
|
|
1158
|
+
app.post("/api/v2/constraints", (req, res) => {
|
|
1159
|
+
setCorsHeaders(res);
|
|
1160
|
+
const auth = authenticateRequest(req);
|
|
1161
|
+
if (auth.authEnabled && (!auth.valid || !checkPermission(auth.role, "speclock_add_lock"))) {
|
|
1162
|
+
return res.status(auth.valid ? 403 : 401).json({ error: "Write permission required." });
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
const { constraint_type, description, tags, ...kwargs } = req.body || {};
|
|
1166
|
+
|
|
1167
|
+
if (!CONSTRAINT_TYPES.includes(constraint_type)) {
|
|
1168
|
+
return res.status(400).json({
|
|
1169
|
+
error: `Invalid constraint_type. Must be one of: ${CONSTRAINT_TYPES.join(", ")}`,
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
try {
|
|
1174
|
+
ensureInit(PROJECT_ROOT);
|
|
1175
|
+
const lockId = addTypedLock(PROJECT_ROOT, { constraintType: constraint_type, ...kwargs }, tags || [], "user", description);
|
|
1176
|
+
return res.json({ success: true, lock_id: lockId, constraint_type });
|
|
1177
|
+
} catch (err) {
|
|
1178
|
+
return res.status(400).json({ error: err.message });
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
// --- v2: List typed constraints ---
|
|
1183
|
+
app.get("/api/v2/constraints", (req, res) => {
|
|
1184
|
+
setCorsHeaders(res);
|
|
1185
|
+
try {
|
|
1186
|
+
ensureInit(PROJECT_ROOT);
|
|
1187
|
+
const brain = readBrain(PROJECT_ROOT);
|
|
1188
|
+
const locks = (brain?.specLock?.items || []).filter(
|
|
1189
|
+
(l) => l.active !== false && CONSTRAINT_TYPES.includes(l.constraintType)
|
|
1190
|
+
);
|
|
1191
|
+
|
|
1192
|
+
const byType = {};
|
|
1193
|
+
for (const ct of CONSTRAINT_TYPES) byType[ct] = 0;
|
|
1194
|
+
for (const l of locks) byType[l.constraintType]++;
|
|
1195
|
+
|
|
1196
|
+
return res.json({
|
|
1197
|
+
total: locks.length,
|
|
1198
|
+
by_type: byType,
|
|
1199
|
+
constraints: locks.map((l) => ({
|
|
1200
|
+
id: l.id,
|
|
1201
|
+
type: l.constraintType,
|
|
1202
|
+
metric: l.metric,
|
|
1203
|
+
entity: l.entity,
|
|
1204
|
+
text: l.text || formatTypedLockText(l),
|
|
1205
|
+
tags: l.tags || [],
|
|
1206
|
+
created: l.createdAt,
|
|
1207
|
+
})),
|
|
1208
|
+
api_version: "v2",
|
|
1209
|
+
});
|
|
1210
|
+
} catch (err) {
|
|
1211
|
+
return res.status(500).json({ error: err.message });
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
// --- v2: Update constraint threshold ---
|
|
1216
|
+
app.put("/api/v2/constraints/:lockId", (req, res) => {
|
|
1217
|
+
setCorsHeaders(res);
|
|
1218
|
+
const auth = authenticateRequest(req);
|
|
1219
|
+
if (auth.authEnabled && (!auth.valid || !checkPermission(auth.role, "speclock_add_lock"))) {
|
|
1220
|
+
return res.status(auth.valid ? 403 : 401).json({ error: "Write permission required." });
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
const { lockId } = req.params;
|
|
1224
|
+
const updates = req.body || {};
|
|
1225
|
+
|
|
1226
|
+
try {
|
|
1227
|
+
ensureInit(PROJECT_ROOT);
|
|
1228
|
+
const result = updateTypedLockThreshold(PROJECT_ROOT, lockId, updates);
|
|
1229
|
+
return res.json({ success: true, ...result });
|
|
1230
|
+
} catch (err) {
|
|
1231
|
+
return res.status(400).json({ error: err.message });
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
// --- v2: Delete constraint ---
|
|
1236
|
+
app.delete("/api/v2/constraints/:lockId", (req, res) => {
|
|
1237
|
+
setCorsHeaders(res);
|
|
1238
|
+
const auth = authenticateRequest(req);
|
|
1239
|
+
if (auth.authEnabled && (!auth.valid || !checkPermission(auth.role, "speclock_add_lock"))) {
|
|
1240
|
+
return res.status(auth.valid ? 403 : 401).json({ error: "Write permission required." });
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
const { lockId } = req.params;
|
|
1244
|
+
try {
|
|
1245
|
+
ensureInit(PROJECT_ROOT);
|
|
1246
|
+
removeLock(PROJECT_ROOT, lockId);
|
|
1247
|
+
return res.json({ success: true, removed: lockId });
|
|
1248
|
+
} catch (err) {
|
|
1249
|
+
return res.status(400).json({ error: err.message });
|
|
1250
|
+
}
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
// --- v2: SSE Stream for real-time constraint status ---
|
|
1254
|
+
// Clients connect once and receive constraint violation events in real-time.
|
|
1255
|
+
// Usage: const evtSource = new EventSource("/api/v2/stream");
|
|
1256
|
+
const sseClients = new Set();
|
|
1257
|
+
|
|
1258
|
+
app.get("/api/v2/stream", (req, res) => {
|
|
1259
|
+
setCorsHeaders(res);
|
|
1260
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
1261
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
1262
|
+
res.setHeader("Connection", "keep-alive");
|
|
1263
|
+
res.flushHeaders();
|
|
1264
|
+
|
|
1265
|
+
// Send initial status
|
|
1266
|
+
try {
|
|
1267
|
+
ensureInit(PROJECT_ROOT);
|
|
1268
|
+
const brain = readBrain(PROJECT_ROOT);
|
|
1269
|
+
const locks = (brain?.specLock?.items || []).filter(
|
|
1270
|
+
(l) => l.active !== false && CONSTRAINT_TYPES.includes(l.constraintType)
|
|
1271
|
+
);
|
|
1272
|
+
const initData = {
|
|
1273
|
+
type: "connected",
|
|
1274
|
+
typed_constraints: locks.length,
|
|
1275
|
+
total_locks: (brain?.specLock?.items || []).filter((l) => l.active !== false).length,
|
|
1276
|
+
timestamp: new Date().toISOString(),
|
|
1277
|
+
};
|
|
1278
|
+
res.write(`event: status\ndata: ${JSON.stringify(initData)}\n\n`);
|
|
1279
|
+
} catch {
|
|
1280
|
+
res.write(`event: status\ndata: ${JSON.stringify({ type: "connected", typed_constraints: 0 })}\n\n`);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// Register client
|
|
1284
|
+
const client = { res, id: Date.now() };
|
|
1285
|
+
sseClients.add(client);
|
|
1286
|
+
|
|
1287
|
+
// Keep alive every 15 seconds
|
|
1288
|
+
const keepAlive = setInterval(() => {
|
|
1289
|
+
res.write(`:keepalive ${new Date().toISOString()}\n\n`);
|
|
1290
|
+
}, 15_000);
|
|
1291
|
+
|
|
1292
|
+
req.on("close", () => {
|
|
1293
|
+
sseClients.delete(client);
|
|
1294
|
+
clearInterval(keepAlive);
|
|
1295
|
+
});
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
// --- v2: Push a check and broadcast violations via SSE ---
|
|
1299
|
+
// POST /api/v2/stream/check — check constraints AND push violations to all SSE clients
|
|
1300
|
+
app.post("/api/v2/stream/check", (req, res) => {
|
|
1301
|
+
setCorsHeaders(res);
|
|
1302
|
+
const clientIp = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket?.remoteAddress || "unknown";
|
|
1303
|
+
if (!checkRateLimit(clientIp)) {
|
|
1304
|
+
return res.status(429).json({ error: "Rate limit exceeded." });
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
const start = performance.now();
|
|
1308
|
+
const { checks } = req.body || {};
|
|
1309
|
+
|
|
1310
|
+
// Accept single check or batch
|
|
1311
|
+
const checkList = Array.isArray(checks) ? checks : [req.body];
|
|
1312
|
+
|
|
1313
|
+
try {
|
|
1314
|
+
ensureInit(PROJECT_ROOT);
|
|
1315
|
+
const brain = readBrain(PROJECT_ROOT);
|
|
1316
|
+
const locks = brain?.specLock?.items || [];
|
|
1317
|
+
const violations = [];
|
|
1318
|
+
|
|
1319
|
+
for (const check of checkList) {
|
|
1320
|
+
const proposed = {};
|
|
1321
|
+
if (check.metric) proposed.metric = check.metric;
|
|
1322
|
+
if (check.entity) proposed.entity = check.entity;
|
|
1323
|
+
if (check.value !== undefined) proposed.value = check.value;
|
|
1324
|
+
if (check.from_state) proposed.from = check.from_state;
|
|
1325
|
+
if (check.to_state) proposed.to = check.to_state;
|
|
1326
|
+
|
|
1327
|
+
const result = checkAllTypedConstraints(locks, proposed);
|
|
1328
|
+
if (result.hasConflict) {
|
|
1329
|
+
const violation = {
|
|
1330
|
+
type: "violation",
|
|
1331
|
+
input: check,
|
|
1332
|
+
conflicts: result.conflictingLocks,
|
|
1333
|
+
analysis: result.analysis,
|
|
1334
|
+
timestamp: new Date().toISOString(),
|
|
1335
|
+
};
|
|
1336
|
+
violations.push(violation);
|
|
1337
|
+
|
|
1338
|
+
// Broadcast to all SSE clients
|
|
1339
|
+
for (const client of sseClients) {
|
|
1340
|
+
try {
|
|
1341
|
+
client.res.write(`event: violation\ndata: ${JSON.stringify(violation)}\n\n`);
|
|
1342
|
+
} catch {
|
|
1343
|
+
sseClients.delete(client);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
const elapsed = performance.now() - start;
|
|
1350
|
+
return res.json({
|
|
1351
|
+
checked: checkList.length,
|
|
1352
|
+
violations: violations.length,
|
|
1353
|
+
emergency_stop: violations.some(
|
|
1354
|
+
(v) => v.conflicts?.some((c) => c.confidence >= 90)
|
|
1355
|
+
),
|
|
1356
|
+
details: violations,
|
|
1357
|
+
sse_clients: sseClients.size,
|
|
1358
|
+
response_time_ms: Number(elapsed.toFixed(3)),
|
|
1359
|
+
api_version: "v2",
|
|
1360
|
+
});
|
|
1361
|
+
} catch (err) {
|
|
1362
|
+
return res.status(500).json({ error: err.message });
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
// --- v2: Real-time system status ---
|
|
1367
|
+
app.get("/api/v2/status", (req, res) => {
|
|
1368
|
+
setCorsHeaders(res);
|
|
1369
|
+
try {
|
|
1370
|
+
ensureInit(PROJECT_ROOT);
|
|
1371
|
+
const brain = readBrain(PROJECT_ROOT);
|
|
1372
|
+
const allLocks = (brain?.specLock?.items || []).filter((l) => l.active !== false);
|
|
1373
|
+
const typedLocks = allLocks.filter((l) => CONSTRAINT_TYPES.includes(l.constraintType));
|
|
1374
|
+
const textLocks = allLocks.filter((l) => !l.constraintType);
|
|
1375
|
+
|
|
1376
|
+
const byType = {};
|
|
1377
|
+
for (const ct of CONSTRAINT_TYPES) byType[ct] = 0;
|
|
1378
|
+
for (const l of typedLocks) byType[l.constraintType]++;
|
|
1379
|
+
|
|
1380
|
+
// Unique metrics and entities being monitored
|
|
1381
|
+
const metrics = [...new Set(typedLocks.filter((l) => l.metric).map((l) => l.metric))];
|
|
1382
|
+
const entities = [...new Set(typedLocks.filter((l) => l.entity).map((l) => l.entity))];
|
|
1383
|
+
|
|
1384
|
+
return res.json({
|
|
1385
|
+
status: "active",
|
|
1386
|
+
version: VERSION,
|
|
1387
|
+
uptime: Math.floor((Date.now() - START_TIME) / 1000),
|
|
1388
|
+
constraints: {
|
|
1389
|
+
typed: typedLocks.length,
|
|
1390
|
+
text: textLocks.length,
|
|
1391
|
+
total: allLocks.length,
|
|
1392
|
+
by_type: byType,
|
|
1393
|
+
},
|
|
1394
|
+
monitoring: {
|
|
1395
|
+
metrics,
|
|
1396
|
+
entities,
|
|
1397
|
+
sse_clients: sseClients.size,
|
|
1398
|
+
},
|
|
1399
|
+
violations: (brain?.state?.violations || []).length,
|
|
1400
|
+
goal: brain?.goal?.text || "",
|
|
1401
|
+
api_version: "v2",
|
|
1402
|
+
});
|
|
1403
|
+
} catch (err) {
|
|
1404
|
+
return res.status(500).json({ error: err.message });
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
// ========================================
|
|
1409
|
+
// SPEC COMPILER ENDPOINTS (v5.0)
|
|
1410
|
+
// ========================================
|
|
1411
|
+
|
|
1412
|
+
app.post("/api/v2/compiler/compile", async (req, res) => {
|
|
1413
|
+
setCorsHeaders(res);
|
|
1414
|
+
const clientIp = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket?.remoteAddress || "unknown";
|
|
1415
|
+
if (!checkRateLimit(clientIp)) {
|
|
1416
|
+
return res.status(429).json({ error: "Rate limit exceeded", api_version: "v2" });
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
try {
|
|
1420
|
+
ensureInit(PROJECT_ROOT);
|
|
1421
|
+
const { text, autoApply } = req.body || {};
|
|
1422
|
+
if (!text || typeof text !== "string") {
|
|
1423
|
+
return res.status(400).json({ error: "Missing or invalid 'text' field", api_version: "v2" });
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
const result = autoApply
|
|
1427
|
+
? await compileAndApply(PROJECT_ROOT, text)
|
|
1428
|
+
: await compileSpec(PROJECT_ROOT, text);
|
|
1429
|
+
|
|
1430
|
+
if (!result.success) {
|
|
1431
|
+
return res.status(400).json({ error: result.error, api_version: "v2" });
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
return res.json({
|
|
1435
|
+
success: true,
|
|
1436
|
+
locks: result.locks,
|
|
1437
|
+
typedLocks: result.typedLocks,
|
|
1438
|
+
decisions: result.decisions,
|
|
1439
|
+
notes: result.notes,
|
|
1440
|
+
summary: result.summary || "",
|
|
1441
|
+
applied: result.applied || null,
|
|
1442
|
+
totalApplied: result.totalApplied || 0,
|
|
1443
|
+
api_version: "v2",
|
|
1444
|
+
});
|
|
1445
|
+
} catch (err) {
|
|
1446
|
+
return res.status(500).json({ error: err.message, api_version: "v2" });
|
|
1447
|
+
}
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
// ========================================
|
|
1451
|
+
// CODE GRAPH ENDPOINTS (v5.0)
|
|
1452
|
+
// ========================================
|
|
1453
|
+
|
|
1454
|
+
app.get("/api/v2/graph", (req, res) => {
|
|
1455
|
+
setCorsHeaders(res);
|
|
1456
|
+
|
|
1457
|
+
try {
|
|
1458
|
+
ensureInit(PROJECT_ROOT);
|
|
1459
|
+
const graph = getOrBuildGraph(PROJECT_ROOT);
|
|
1460
|
+
return res.json({ ...graph, api_version: "v2" });
|
|
1461
|
+
} catch (err) {
|
|
1462
|
+
return res.status(500).json({ error: err.message, api_version: "v2" });
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
app.post("/api/v2/graph/build", (req, res) => {
|
|
1467
|
+
setCorsHeaders(res);
|
|
1468
|
+
|
|
1469
|
+
try {
|
|
1470
|
+
ensureInit(PROJECT_ROOT);
|
|
1471
|
+
const graph = buildGraph(PROJECT_ROOT, { force: true });
|
|
1472
|
+
return res.json({
|
|
1473
|
+
success: true,
|
|
1474
|
+
stats: graph.stats,
|
|
1475
|
+
builtAt: graph.builtAt,
|
|
1476
|
+
api_version: "v2",
|
|
1477
|
+
});
|
|
1478
|
+
} catch (err) {
|
|
1479
|
+
return res.status(500).json({ error: err.message, api_version: "v2" });
|
|
1480
|
+
}
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
app.get("/api/v2/graph/blast-radius", (req, res) => {
|
|
1484
|
+
setCorsHeaders(res);
|
|
1485
|
+
|
|
1486
|
+
try {
|
|
1487
|
+
ensureInit(PROJECT_ROOT);
|
|
1488
|
+
const file = req.query?.file;
|
|
1489
|
+
if (!file) {
|
|
1490
|
+
return res.status(400).json({ error: "Missing 'file' query parameter", api_version: "v2" });
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
const result = getBlastRadius(PROJECT_ROOT, file);
|
|
1494
|
+
return res.json({ ...result, api_version: "v2" });
|
|
1495
|
+
} catch (err) {
|
|
1496
|
+
return res.status(500).json({ error: err.message, api_version: "v2" });
|
|
1497
|
+
}
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
app.get("/api/v2/graph/lock-map", (req, res) => {
|
|
1501
|
+
setCorsHeaders(res);
|
|
1502
|
+
|
|
1503
|
+
try {
|
|
1504
|
+
ensureInit(PROJECT_ROOT);
|
|
1505
|
+
const mappings = mapLocksToFiles(PROJECT_ROOT);
|
|
1506
|
+
return res.json({ mappings, count: mappings.length, api_version: "v2" });
|
|
1507
|
+
} catch (err) {
|
|
1508
|
+
return res.status(500).json({ error: err.message, api_version: "v2" });
|
|
1509
|
+
}
|
|
1510
|
+
});
|
|
1511
|
+
|
|
925
1512
|
// ========================================
|
|
926
1513
|
// SSO ENDPOINTS (v3.5)
|
|
927
1514
|
// ========================================
|
|
@@ -962,5 +1549,7 @@ app.post("/auth/sso/logout", (req, res) => {
|
|
|
962
1549
|
const PORT = parseInt(process.env.PORT || "3000", 10);
|
|
963
1550
|
app.listen(PORT, "0.0.0.0", () => {
|
|
964
1551
|
console.log(`SpecLock MCP HTTP Server v${VERSION} running on port ${PORT} — Developed by ${AUTHOR}`);
|
|
965
|
-
console.log(` Dashboard:
|
|
1552
|
+
console.log(` Dashboard: http://localhost:${PORT}/dashboard`);
|
|
1553
|
+
console.log(` REST API v2: http://localhost:${PORT}/api/v2/status`);
|
|
1554
|
+
console.log(` SSE Stream: http://localhost:${PORT}/api/v2/stream`);
|
|
966
1555
|
});
|