speclock 3.0.0 → 3.5.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 +76 -4
- package/package.json +10 -3
- package/src/cli/index.js +174 -1
- package/src/core/compliance.js +1 -1
- package/src/core/engine.js +38 -0
- package/src/core/policy.js +719 -0
- package/src/core/sso.js +386 -0
- package/src/core/telemetry.js +281 -0
- package/src/dashboard/index.html +338 -0
- package/src/mcp/http-server.js +157 -2
- package/src/mcp/server.js +149 -1
package/src/mcp/server.js
CHANGED
|
@@ -33,6 +33,16 @@ import {
|
|
|
33
33
|
getOverrideHistory,
|
|
34
34
|
getEnforcementConfig,
|
|
35
35
|
semanticAudit,
|
|
36
|
+
evaluatePolicy,
|
|
37
|
+
listPolicyRules,
|
|
38
|
+
addPolicyRule,
|
|
39
|
+
removePolicyRule,
|
|
40
|
+
initPolicy,
|
|
41
|
+
exportPolicy,
|
|
42
|
+
importPolicy,
|
|
43
|
+
isTelemetryEnabled,
|
|
44
|
+
getTelemetrySummary,
|
|
45
|
+
trackToolUsage,
|
|
36
46
|
} from "../core/engine.js";
|
|
37
47
|
import { generateContext, generateContextPack } from "../core/context.js";
|
|
38
48
|
import {
|
|
@@ -90,7 +100,7 @@ const PROJECT_ROOT =
|
|
|
90
100
|
args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
|
|
91
101
|
|
|
92
102
|
// --- MCP Server ---
|
|
93
|
-
const VERSION = "3.
|
|
103
|
+
const VERSION = "3.5.0";
|
|
94
104
|
const AUTHOR = "Sandeep Roy";
|
|
95
105
|
|
|
96
106
|
const server = new McpServer(
|
|
@@ -1160,6 +1170,144 @@ server.tool(
|
|
|
1160
1170
|
}
|
|
1161
1171
|
);
|
|
1162
1172
|
|
|
1173
|
+
// ========================================
|
|
1174
|
+
// POLICY-AS-CODE TOOLS (v3.5)
|
|
1175
|
+
// ========================================
|
|
1176
|
+
|
|
1177
|
+
// Tool 29: speclock_policy_evaluate
|
|
1178
|
+
server.tool(
|
|
1179
|
+
"speclock_policy_evaluate",
|
|
1180
|
+
"Evaluate policy-as-code rules against a proposed action. Returns violations for any matching rules. Use alongside speclock_check_conflict for comprehensive protection.",
|
|
1181
|
+
{
|
|
1182
|
+
description: z.string().min(1).describe("Description of the action to evaluate"),
|
|
1183
|
+
files: z.array(z.string()).optional().default([]).describe("Files affected by the action"),
|
|
1184
|
+
type: z.enum(["modify", "delete", "create", "export"]).optional().default("modify").describe("Action type"),
|
|
1185
|
+
},
|
|
1186
|
+
async ({ description, files, type }) => {
|
|
1187
|
+
const result = evaluatePolicy(PROJECT_ROOT, { description, text: description, files, type });
|
|
1188
|
+
|
|
1189
|
+
if (result.passed) {
|
|
1190
|
+
return {
|
|
1191
|
+
content: [{ type: "text", text: `Policy check passed. ${result.rulesChecked} rule(s) evaluated, no violations.` }],
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
const formatted = result.violations
|
|
1196
|
+
.map(v => `- [${v.severity.toUpperCase()}] **${v.ruleName}** (${v.enforce})\n ${v.description}\n Files: ${v.matchedFiles.join(", ") || "(pattern match)"}`)
|
|
1197
|
+
.join("\n\n");
|
|
1198
|
+
|
|
1199
|
+
return {
|
|
1200
|
+
content: [{ type: "text", text: `## Policy Violations (${result.violations.length})\n\n${formatted}` }],
|
|
1201
|
+
isError: result.blocked,
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
);
|
|
1205
|
+
|
|
1206
|
+
// Tool 30: speclock_policy_manage
|
|
1207
|
+
server.tool(
|
|
1208
|
+
"speclock_policy_manage",
|
|
1209
|
+
"Manage policy-as-code rules. Actions: list (show all rules), add (create new rule), remove (delete rule), init (create default policy), export (portable YAML).",
|
|
1210
|
+
{
|
|
1211
|
+
action: z.enum(["list", "add", "remove", "init", "export"]).describe("Policy action"),
|
|
1212
|
+
rule: z.object({
|
|
1213
|
+
name: z.string().optional(),
|
|
1214
|
+
description: z.string().optional(),
|
|
1215
|
+
match: z.object({
|
|
1216
|
+
files: z.array(z.string()).optional(),
|
|
1217
|
+
actions: z.array(z.string()).optional(),
|
|
1218
|
+
}).optional(),
|
|
1219
|
+
enforce: z.enum(["block", "warn", "log"]).optional(),
|
|
1220
|
+
severity: z.enum(["critical", "high", "medium", "low"]).optional(),
|
|
1221
|
+
notify: z.array(z.string()).optional(),
|
|
1222
|
+
}).optional().describe("Rule definition (for add action)"),
|
|
1223
|
+
ruleId: z.string().optional().describe("Rule ID (for remove action)"),
|
|
1224
|
+
},
|
|
1225
|
+
async ({ action, rule, ruleId }) => {
|
|
1226
|
+
switch (action) {
|
|
1227
|
+
case "list": {
|
|
1228
|
+
const result = listPolicyRules(PROJECT_ROOT);
|
|
1229
|
+
if (result.total === 0) {
|
|
1230
|
+
return { content: [{ type: "text", text: "No policy rules defined. Use action 'init' to create a default policy." }] };
|
|
1231
|
+
}
|
|
1232
|
+
const formatted = result.rules.map(r =>
|
|
1233
|
+
`- **${r.name}** (${r.id}) [${r.enforce}/${r.severity}]\n Files: ${(r.match?.files || []).join(", ")}\n Actions: ${(r.match?.actions || []).join(", ")}`
|
|
1234
|
+
).join("\n\n");
|
|
1235
|
+
return { content: [{ type: "text", text: `## Policy Rules (${result.active}/${result.total} active)\n\n${formatted}` }] };
|
|
1236
|
+
}
|
|
1237
|
+
case "add": {
|
|
1238
|
+
if (!rule || !rule.name) {
|
|
1239
|
+
return { content: [{ type: "text", text: "Rule name is required." }], isError: true };
|
|
1240
|
+
}
|
|
1241
|
+
const result = addPolicyRule(PROJECT_ROOT, rule);
|
|
1242
|
+
if (!result.success) return { content: [{ type: "text", text: result.error }], isError: true };
|
|
1243
|
+
return { content: [{ type: "text", text: `Policy rule added: "${result.rule.name}" (${result.ruleId}) [${result.rule.enforce}]` }] };
|
|
1244
|
+
}
|
|
1245
|
+
case "remove": {
|
|
1246
|
+
if (!ruleId) return { content: [{ type: "text", text: "ruleId is required." }], isError: true };
|
|
1247
|
+
const result = removePolicyRule(PROJECT_ROOT, ruleId);
|
|
1248
|
+
if (!result.success) return { content: [{ type: "text", text: result.error }], isError: true };
|
|
1249
|
+
return { content: [{ type: "text", text: `Policy rule removed: "${result.removed.name}"` }] };
|
|
1250
|
+
}
|
|
1251
|
+
case "init": {
|
|
1252
|
+
const result = initPolicy(PROJECT_ROOT);
|
|
1253
|
+
if (!result.success) return { content: [{ type: "text", text: result.error }], isError: true };
|
|
1254
|
+
return { content: [{ type: "text", text: "Policy-as-code initialized. Edit .speclock/policy.yml to add rules." }] };
|
|
1255
|
+
}
|
|
1256
|
+
case "export": {
|
|
1257
|
+
const result = exportPolicy(PROJECT_ROOT);
|
|
1258
|
+
if (!result.success) return { content: [{ type: "text", text: result.error }], isError: true };
|
|
1259
|
+
return { content: [{ type: "text", text: `## Exported Policy\n\n\`\`\`yaml\n${result.yaml}\`\`\`` }] };
|
|
1260
|
+
}
|
|
1261
|
+
default:
|
|
1262
|
+
return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
);
|
|
1266
|
+
|
|
1267
|
+
// ========================================
|
|
1268
|
+
// TELEMETRY TOOLS (v3.5)
|
|
1269
|
+
// ========================================
|
|
1270
|
+
|
|
1271
|
+
// Tool 31: speclock_telemetry
|
|
1272
|
+
server.tool(
|
|
1273
|
+
"speclock_telemetry",
|
|
1274
|
+
"Get telemetry and analytics summary. Shows tool usage counts, conflict rates, response times, and feature adoption. Opt-in only (SPECLOCK_TELEMETRY=true).",
|
|
1275
|
+
{},
|
|
1276
|
+
async () => {
|
|
1277
|
+
const summary = getTelemetrySummary(PROJECT_ROOT);
|
|
1278
|
+
if (!summary.enabled) {
|
|
1279
|
+
return { content: [{ type: "text", text: summary.message }] };
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
const parts = [
|
|
1283
|
+
`## Telemetry Summary`,
|
|
1284
|
+
``,
|
|
1285
|
+
`Total API calls: **${summary.totalCalls}**`,
|
|
1286
|
+
`Avg response: **${summary.avgResponseMs}ms**`,
|
|
1287
|
+
`Sessions: **${summary.sessions.total}**`,
|
|
1288
|
+
``,
|
|
1289
|
+
`### Conflicts`,
|
|
1290
|
+
`Total: ${summary.conflicts.total} | Blocked: ${summary.conflicts.blocked} | Advisory: ${summary.conflicts.advisory}`,
|
|
1291
|
+
];
|
|
1292
|
+
|
|
1293
|
+
if (summary.topTools.length > 0) {
|
|
1294
|
+
parts.push(``, `### Top Tools`);
|
|
1295
|
+
for (const t of summary.topTools.slice(0, 5)) {
|
|
1296
|
+
parts.push(`- ${t.name}: ${t.count} calls (avg ${t.avgMs}ms)`);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
if (summary.features.length > 0) {
|
|
1301
|
+
parts.push(``, `### Feature Adoption`);
|
|
1302
|
+
for (const f of summary.features) {
|
|
1303
|
+
parts.push(`- ${f.name}: ${f.count} uses`);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1308
|
+
}
|
|
1309
|
+
);
|
|
1310
|
+
|
|
1163
1311
|
// --- Smithery sandbox export ---
|
|
1164
1312
|
export default function createSandboxServer() {
|
|
1165
1313
|
return server;
|