@rigour-labs/mcp 5.5.4 → 6.1.0-beta.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/dist/index.js +87 -5
- package/dist/tools/agent-handlers.js +44 -11
- package/dist/tools/context-handlers.js +66 -19
- package/dist/tools/execution-handlers.d.ts +13 -1
- package/dist/tools/execution-handlers.js +114 -38
- package/dist/tools/hooks-handler.js +6 -1
- package/dist/tools/index-handlers.js +1 -1
- package/dist/tools/memory-handlers.js +24 -4
- package/dist/tools/pattern-handlers.js +24 -1
- package/dist/utils/context-telemetry.d.ts +29 -0
- package/dist/utils/context-telemetry.js +4 -2
- package/dist/utils/impact-receipt.d.ts +13 -0
- package/dist/utils/impact-receipt.js +43 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { GateRunner } from "@rigour-labs/core";
|
|
|
16
16
|
import { loadConfig, loadMcpSettings, logStudioEvent } from './utils/config.js';
|
|
17
17
|
import { bindServer } from './utils/notifications.js';
|
|
18
18
|
import { getMcpVersion } from './utils/package-version.js';
|
|
19
|
+
import { buildMcpResultMeta, buildStudioImpact } from './utils/impact-receipt.js';
|
|
19
20
|
// Dashboard (MCP App)
|
|
20
21
|
import { DASHBOARD_URI, getDashboardHtml, pushTimelineEntry, updateScore } from './dashboard/index.js';
|
|
21
22
|
// Tool definitions & advertised registry
|
|
@@ -32,9 +33,25 @@ import { handleCheckDeep, handleDeepStats } from './tools/deep-handlers.js';
|
|
|
32
33
|
import { handleMcpGetSettings, handleMcpSetSettings } from './tools/mcp-settings-handler.js';
|
|
33
34
|
import { handleContextStats, handleTaskCost, handleCacheStats, handleContextExplain, handleContextScope } from './tools/context-handlers.js';
|
|
34
35
|
import { handleIndex } from './tools/index-handlers.js';
|
|
35
|
-
import { recordContextEvent, estimateTokenCount } from '@rigour-labs/core';
|
|
36
|
+
import { recordContextEvent, recordInteractionEvidence, recordInteractionLesson, syncTeamOutbox, estimateTokenCount } from '@rigour-labs/core';
|
|
36
37
|
// ─── Server Setup ─────────────────────────────────────────────────
|
|
37
38
|
const server = new Server({ name: "rigour-mcp", version: getMcpVersion('5.5.0') }, { capabilities: { tools: {}, prompts: {}, logging: {}, resources: {} } });
|
|
39
|
+
function interactionMetadata(args) {
|
|
40
|
+
const input = (args && typeof args === 'object' ? args : {});
|
|
41
|
+
const rawFiles = Array.isArray(input.files)
|
|
42
|
+
? input.files
|
|
43
|
+
: Array.isArray(input.filesChanged)
|
|
44
|
+
? input.filesChanged
|
|
45
|
+
: Array.isArray(input.filesInScope)
|
|
46
|
+
? input.filesInScope
|
|
47
|
+
: input.file ? [input.file] : [];
|
|
48
|
+
return {
|
|
49
|
+
taskId: String(input.taskId || input.task_id || '') || undefined,
|
|
50
|
+
agentId: String(input.agentId || input.agent_id || input.agent || '') || undefined,
|
|
51
|
+
sessionId: String(input.sessionId || input.session_id || '') || undefined,
|
|
52
|
+
files: rawFiles.filter((file) => typeof file === 'string').slice(0, 100),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
38
55
|
// Bind server for logging notifications from handlers
|
|
39
56
|
bindServer(server);
|
|
40
57
|
// ─── Tool Listing ─────────────────────────────────────────────────
|
|
@@ -69,6 +86,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
69
86
|
const requestId = randomUUID();
|
|
70
87
|
try {
|
|
71
88
|
await logStudioEvent(cwd, { type: "tool_call", requestId, tool: name, arguments: args });
|
|
89
|
+
try {
|
|
90
|
+
await recordInteractionEvidence(cwd, {
|
|
91
|
+
tool: name,
|
|
92
|
+
outcome: 'success',
|
|
93
|
+
phase: 'request',
|
|
94
|
+
requestId,
|
|
95
|
+
...interactionMetadata(args),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Evidence capture must never block the requested governance operation.
|
|
100
|
+
}
|
|
72
101
|
// ── Image DLP warning ──────────────────────────────
|
|
73
102
|
// MCP args may contain base64 image data. Text DLP cannot scan images.
|
|
74
103
|
const argsStr = JSON.stringify(args ?? {});
|
|
@@ -136,7 +165,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
136
165
|
break;
|
|
137
166
|
case "rigour_run_supervised": {
|
|
138
167
|
const { command, maxRetries = 3, dryRun = false } = args;
|
|
139
|
-
result = await handleRunSupervised(
|
|
168
|
+
result = await handleRunSupervised({
|
|
169
|
+
runner,
|
|
170
|
+
cwd,
|
|
171
|
+
command,
|
|
172
|
+
maxRetries,
|
|
173
|
+
dryRun,
|
|
174
|
+
requestId,
|
|
175
|
+
config,
|
|
176
|
+
});
|
|
140
177
|
break;
|
|
141
178
|
}
|
|
142
179
|
// Multi-agent governance
|
|
@@ -225,12 +262,41 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
225
262
|
candidateTokens: telemetry?.candidateTokens ?? returnedTokens,
|
|
226
263
|
returnedTokens,
|
|
227
264
|
deduplicatedTokens: telemetry?.deduplicatedTokens ?? 0,
|
|
265
|
+
candidateFiles: telemetry?.candidateFiles,
|
|
266
|
+
returnedFiles: telemetry?.returnedFiles,
|
|
228
267
|
}, cwd);
|
|
229
268
|
}
|
|
230
269
|
}
|
|
231
270
|
catch {
|
|
232
271
|
// non-blocking context telemetry logging
|
|
233
272
|
}
|
|
273
|
+
try {
|
|
274
|
+
const reportStatus = result?._rigour_report?.status;
|
|
275
|
+
const deterministic = Boolean(reportStatus === 'PASS');
|
|
276
|
+
const outcome = result?.isError ? 'error' : 'success';
|
|
277
|
+
await recordInteractionEvidence(cwd, {
|
|
278
|
+
tool: name,
|
|
279
|
+
outcome,
|
|
280
|
+
phase: 'response',
|
|
281
|
+
requestId,
|
|
282
|
+
deterministic,
|
|
283
|
+
summary: result?._guidance?.recommendation,
|
|
284
|
+
...interactionMetadata(args),
|
|
285
|
+
});
|
|
286
|
+
await recordInteractionLesson(cwd, {
|
|
287
|
+
tool: name,
|
|
288
|
+
outcome,
|
|
289
|
+
requestId,
|
|
290
|
+
deterministic,
|
|
291
|
+
verifiedOutcome: deterministic,
|
|
292
|
+
summary: result?._guidance?.recommendation,
|
|
293
|
+
...interactionMetadata(args),
|
|
294
|
+
});
|
|
295
|
+
void syncTeamOutbox().catch(() => undefined);
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
// Learning is evidence collection and must not block governance tools.
|
|
299
|
+
}
|
|
234
300
|
// ── Prepend image DLP warning if image content was detected ──
|
|
235
301
|
if (hasImageContent && Array.isArray(result.content)) {
|
|
236
302
|
result.content.unshift({
|
|
@@ -238,9 +304,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
238
304
|
text: "⚠ DLP Notice: Image content detected in this request. Text-based credential scanning cannot analyze images. Avoid sharing screenshots containing API keys, tokens, or passwords.",
|
|
239
305
|
});
|
|
240
306
|
}
|
|
307
|
+
const studioImpact = buildStudioImpact(result._guidance, result._telemetry);
|
|
241
308
|
await logStudioEvent(cwd, {
|
|
242
|
-
type: "tool_response", requestId, tool: name, status: "success",
|
|
243
|
-
content: result.content,
|
|
309
|
+
type: "tool_response", requestId, tool: name, status: result.isError ? "error" : "success",
|
|
310
|
+
content: result.content,
|
|
311
|
+
_rigour_report: result._rigour_report,
|
|
312
|
+
...studioImpact,
|
|
244
313
|
});
|
|
245
314
|
// ─── Dashboard State + MCP App UI Hint ──────────────
|
|
246
315
|
const report = result._rigour_report;
|
|
@@ -253,7 +322,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
253
322
|
}
|
|
254
323
|
// Attach UI hint for MCP-App-capable clients (Claude Desktop, VS Code, ChatGPT, Goose)
|
|
255
324
|
if (!result.isError) {
|
|
256
|
-
result._meta =
|
|
325
|
+
result._meta = buildMcpResultMeta(result._meta, requestId, DASHBOARD_URI, result._guidance, result._telemetry);
|
|
257
326
|
}
|
|
258
327
|
// Notify clients that the dashboard resource has updated
|
|
259
328
|
try {
|
|
@@ -272,6 +341,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
272
341
|
content: [{ type: "text", text: `RIGOUR ERROR: ${error.message}` }],
|
|
273
342
|
isError: true,
|
|
274
343
|
};
|
|
344
|
+
try {
|
|
345
|
+
await recordInteractionEvidence(cwd, {
|
|
346
|
+
tool: name,
|
|
347
|
+
outcome: 'error',
|
|
348
|
+
phase: 'response',
|
|
349
|
+
requestId,
|
|
350
|
+
summary: String(error?.message || 'Unknown error').slice(0, 500),
|
|
351
|
+
...interactionMetadata(args),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
// Failure evidence capture is intentionally non-blocking.
|
|
356
|
+
}
|
|
275
357
|
await logStudioEvent(cwd, {
|
|
276
358
|
type: "tool_response", requestId, tool: name,
|
|
277
359
|
status: "error", error: error.message, content: errorResponse.content,
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
import fs from "fs-extra";
|
|
10
10
|
import path from "path";
|
|
11
11
|
import { logStudioEvent } from '../utils/config.js';
|
|
12
|
-
import { setTaskCheckpointCache, getTaskCheckpointCache, estimateTokenCount, } from '@rigour-labs/core';
|
|
13
|
-
import {
|
|
12
|
+
import { setTaskCheckpointCache, getTaskCheckpointCache, estimateTokenCount, updateAutomaticIndexForFiles, } from '@rigour-labs/core';
|
|
13
|
+
import { loadPatternIndex, getDefaultIndexPath, } from '@rigour-labs/core/pattern-index';
|
|
14
14
|
import { buildTelemetryMeta, getWorkspaceCommitSha } from '../utils/context-telemetry.js';
|
|
15
15
|
import { appendContextFooter } from '../utils/context-footer.js';
|
|
16
16
|
const HANDOFF_CONTEXT_TOKEN_LIMIT = 2000;
|
|
@@ -38,16 +38,9 @@ function globMatchesFile(filePath, glob) {
|
|
|
38
38
|
async function refreshIndexForFiles(cwd, filesChanged) {
|
|
39
39
|
if (filesChanged.length === 0)
|
|
40
40
|
return null;
|
|
41
|
-
const indexPath = getDefaultIndexPath(cwd);
|
|
42
|
-
const existingIndex = await loadPatternIndex(indexPath);
|
|
43
|
-
if (!existingIndex) {
|
|
44
|
-
return 'Index not found — run rigour_index to enable incremental updates.';
|
|
45
|
-
}
|
|
46
41
|
try {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
await savePatternIndex(updated, indexPath);
|
|
50
|
-
return `Index refreshed for ${filesChanged.length} changed file(s) — ${updated.stats.totalPatterns} patterns total.`;
|
|
42
|
+
const { index, affectedFiles } = await updateAutomaticIndexForFiles(cwd, filesChanged);
|
|
43
|
+
return `Index refreshed ${filesChanged.length} changed and ${affectedFiles.length} affected file(s) — ${index.stats.totalPatterns} patterns total.`;
|
|
51
44
|
}
|
|
52
45
|
catch (error) {
|
|
53
46
|
return `Index refresh failed: ${error.message}`;
|
|
@@ -82,6 +75,46 @@ async function validateTaskScopeAgainstIndex(cwd, taskScope) {
|
|
|
82
75
|
}
|
|
83
76
|
// ─── Agent Register ───────────────────────────────────────────────
|
|
84
77
|
export async function handleAgentRegister(cwd, agentId, taskScope, requestId) {
|
|
78
|
+
const { validateAgentClaimedScope, loadOperatorScopes, isScopeSubset, } = await import('@rigour-labs/core');
|
|
79
|
+
const authority = process.env.RIGOUR_ALLOW_AGENT_SCOPE_AUTHORITY === '1';
|
|
80
|
+
if (!authority) {
|
|
81
|
+
const claimCheck = validateAgentClaimedScope(taskScope);
|
|
82
|
+
if (!claimCheck.ok) {
|
|
83
|
+
await logStudioEvent(cwd, {
|
|
84
|
+
type: 'firewall_deny',
|
|
85
|
+
requestId,
|
|
86
|
+
tool: 'rigour_agent_register',
|
|
87
|
+
decision: 'deny',
|
|
88
|
+
reason: claimCheck.reason,
|
|
89
|
+
ruleId: 'scope.register-denied',
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
content: [{ type: 'text', text: `❌ SCOPE REGISTER DENIED: ${claimCheck.reason}` }],
|
|
93
|
+
isError: true,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const operatorScopes = await loadOperatorScopes(cwd);
|
|
98
|
+
if (operatorScopes) {
|
|
99
|
+
const allowed = operatorScopes[agentId];
|
|
100
|
+
if (!allowed || !isScopeSubset(taskScope, allowed)) {
|
|
101
|
+
const msg = allowed
|
|
102
|
+
? `Claimed scope is not a subset of operator scopes for "${agentId}": ${allowed.join(', ')}`
|
|
103
|
+
: `No operator scopes defined for agent "${agentId}" in .rigour/operator-scopes.json`;
|
|
104
|
+
await logStudioEvent(cwd, {
|
|
105
|
+
type: 'firewall_deny',
|
|
106
|
+
requestId,
|
|
107
|
+
tool: 'rigour_agent_register',
|
|
108
|
+
decision: 'deny',
|
|
109
|
+
reason: msg,
|
|
110
|
+
ruleId: 'scope.operator-required',
|
|
111
|
+
});
|
|
112
|
+
return {
|
|
113
|
+
content: [{ type: 'text', text: `❌ SCOPE REGISTER DENIED: ${msg}` }],
|
|
114
|
+
isError: true,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
85
118
|
const sessionPath = path.join(cwd, '.rigour', 'agent-session.json');
|
|
86
119
|
let session = { agents: [], startedAt: new Date().toISOString() };
|
|
87
120
|
if (await fs.pathExists(sessionPath)) {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import fs from 'fs-extra';
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import { createHash } from 'crypto';
|
|
14
|
-
import { getTaskContextStats, getTaskCostStats, getCacheStats, explainContext, getSemanticQueryCache, setSemanticQueryCache, findRelatedSemanticQueryCache, filterExistingEditScope, estimateTokenCount, } from '@rigour-labs/core';
|
|
14
|
+
import { getTaskContextStats, getTaskCostStats, getCacheStats, explainContext, getSemanticQueryCache, setSemanticQueryCache, findRelatedSemanticQueryCache, filterExistingEditScope, estimateTokenCount, ensureAutomaticIndex, getApplicableLessons, searchTeamKnowledge, } from '@rigour-labs/core';
|
|
15
15
|
import { loadPatternIndex, getDefaultIndexPath, generateEmbedding, semanticSearch, } from '@rigour-labs/core/pattern-index';
|
|
16
16
|
import { buildTelemetryMeta, getWorkspaceCommitSha } from '../utils/context-telemetry.js';
|
|
17
17
|
import { appendContextFooter } from '../utils/context-footer.js';
|
|
@@ -107,6 +107,10 @@ function scopeTelemetryMeta(opts) {
|
|
|
107
107
|
taskId: opts.taskId,
|
|
108
108
|
agentId: opts.agentId,
|
|
109
109
|
queryHash: hashScopeQuery(opts.query),
|
|
110
|
+
candidateTokens: opts.candidateTokens,
|
|
111
|
+
returnedTokens: opts.returnedTokens,
|
|
112
|
+
candidateFiles: opts.candidateFiles,
|
|
113
|
+
returnedFiles: opts.returnedFiles,
|
|
110
114
|
});
|
|
111
115
|
}
|
|
112
116
|
function textMatchPatterns(query, patterns, limit) {
|
|
@@ -155,6 +159,7 @@ async function serveCachedScope(cached, opts) {
|
|
|
155
159
|
cacheNote: opts.note,
|
|
156
160
|
};
|
|
157
161
|
const cachedText = JSON.stringify(payload, null, 2);
|
|
162
|
+
const returnedTokens = estimateTokenCount(cachedText);
|
|
158
163
|
const telemetry = scopeTelemetryMeta({
|
|
159
164
|
candidateText: opts.fullIndexCandidate,
|
|
160
165
|
returnedText: cachedText,
|
|
@@ -162,6 +167,11 @@ async function serveCachedScope(cached, opts) {
|
|
|
162
167
|
query: opts.query,
|
|
163
168
|
taskId: opts.taskId,
|
|
164
169
|
agentId: opts.agentId,
|
|
170
|
+
candidateTokens: cached.sourceTokens,
|
|
171
|
+
returnedTokens,
|
|
172
|
+
candidateFiles: cached.candidateFiles,
|
|
173
|
+
returnedFiles: valid.length,
|
|
174
|
+
deduplicatedTokens: Math.max(0, (cached.sourceTokens ?? returnedTokens) - returnedTokens),
|
|
165
175
|
});
|
|
166
176
|
const label = opts.cacheStatus === 'partial-hit'
|
|
167
177
|
? 'CONTEXT SCOPE (partial cache)'
|
|
@@ -170,6 +180,15 @@ async function serveCachedScope(cached, opts) {
|
|
|
170
180
|
return {
|
|
171
181
|
content: [{ type: 'text', text: appendContextFooter(text, telemetry, 'rigour_check_pattern before creating code') }],
|
|
172
182
|
_telemetry: telemetry,
|
|
183
|
+
_guidance: {
|
|
184
|
+
...(cached.guidance ?? {
|
|
185
|
+
kind: 'context-scope',
|
|
186
|
+
recommendation: `Reuse the cached scope of ${valid.length} file(s).`,
|
|
187
|
+
}),
|
|
188
|
+
query: opts.query,
|
|
189
|
+
selectedFiles: valid,
|
|
190
|
+
excludedFileCount: Math.max(0, (cached.candidateFiles ?? valid.length) - valid.length),
|
|
191
|
+
},
|
|
173
192
|
};
|
|
174
193
|
}
|
|
175
194
|
export async function handleContextScope(cwd, query, limit = 10, taskId, agentId) {
|
|
@@ -205,22 +224,9 @@ export async function handleContextScope(cwd, query, limit = 10, taskId, agentId
|
|
|
205
224
|
if (served)
|
|
206
225
|
return served;
|
|
207
226
|
}
|
|
208
|
-
const index = await loadPatternIndex(indexPath)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
+ 'Without an index, agents tend to read entire directories — wasting tokens.';
|
|
212
|
-
return {
|
|
213
|
-
content: [{ type: 'text', text }],
|
|
214
|
-
_telemetry: scopeTelemetryMeta({
|
|
215
|
-
candidateText: fullIndexCandidate,
|
|
216
|
-
returnedText: text,
|
|
217
|
-
cacheStatus: 'miss',
|
|
218
|
-
query,
|
|
219
|
-
taskId,
|
|
220
|
-
agentId,
|
|
221
|
-
}),
|
|
222
|
-
};
|
|
223
|
-
}
|
|
227
|
+
const index = await loadPatternIndex(indexPath) ?? await ensureAutomaticIndex(cwd, {
|
|
228
|
+
backgroundSemantic: process.env.NODE_ENV !== 'test',
|
|
229
|
+
});
|
|
224
230
|
let matched = [];
|
|
225
231
|
const hasEmbeddings = index.patterns.some(p => p.embedding && p.embedding.length > 0);
|
|
226
232
|
if (hasEmbeddings) {
|
|
@@ -252,6 +258,10 @@ export async function handleContextScope(cwd, query, limit = 10, taskId, agentId
|
|
|
252
258
|
.slice(0, Math.max(0, 20 - editScope.length))
|
|
253
259
|
.map(f => ({ file: f, reason: 'Outside semantic match for query' }));
|
|
254
260
|
const signatures = matched.map(p => `- ${p.file}:${p.line} ${p.type} ${p.name} — ${p.signature || p.description || '(no signature)'}`);
|
|
261
|
+
const [applicableLearning, semanticKnowledge] = await Promise.all([
|
|
262
|
+
getApplicableLessons(cwd, agentId, process.env.RIGOUR_TEAM_ID),
|
|
263
|
+
searchTeamKnowledge(query, Math.min(limit, 8)),
|
|
264
|
+
]);
|
|
255
265
|
let fullFileTokens = 0;
|
|
256
266
|
for (const file of editScope) {
|
|
257
267
|
try {
|
|
@@ -262,22 +272,51 @@ export async function handleContextScope(cwd, query, limit = 10, taskId, agentId
|
|
|
262
272
|
fullFileTokens += 500;
|
|
263
273
|
}
|
|
264
274
|
}
|
|
275
|
+
const guidance = {
|
|
276
|
+
kind: 'context-scope',
|
|
277
|
+
query,
|
|
278
|
+
recommendation: editScope.length > 0
|
|
279
|
+
? `Read only the ${editScope.length} evidence-backed file(s) in this scope.`
|
|
280
|
+
: 'Refine the task query before reading broadly.',
|
|
281
|
+
patternRefs: matched.map(pattern => ({ id: pattern.id, label: pattern.name, file: pattern.file })),
|
|
282
|
+
lessonRefs: applicableLearning.lessons.map(lesson => ({ id: lesson.id, label: lesson.subject, visibility: lesson.visibility })),
|
|
283
|
+
selectedFiles: editScope,
|
|
284
|
+
excludedFileCount: Math.max(0, index.stats.totalFiles - editScope.length),
|
|
285
|
+
conflicts: applicableLearning.conflicts.length,
|
|
286
|
+
};
|
|
265
287
|
const scopePayload = {
|
|
266
288
|
query,
|
|
267
289
|
editScope,
|
|
268
290
|
skipScope,
|
|
269
291
|
signatures,
|
|
292
|
+
learningGuidance: applicableLearning.lessons.map(lesson => ({
|
|
293
|
+
subject: lesson.subject,
|
|
294
|
+
scope: lesson.repositoryId,
|
|
295
|
+
visibility: lesson.visibility,
|
|
296
|
+
confidence: lesson.confidence,
|
|
297
|
+
provenance: lesson.provenance,
|
|
298
|
+
})),
|
|
299
|
+
learningConflicts: applicableLearning.conflicts,
|
|
300
|
+
semanticKnowledge: {
|
|
301
|
+
...semanticKnowledge,
|
|
302
|
+
purpose: 'Advisory cross-repository recall only; similarity never promotes or enforces a lesson.',
|
|
303
|
+
},
|
|
270
304
|
indexHealth: {
|
|
271
305
|
totalPatterns: index.stats.totalPatterns,
|
|
272
306
|
totalFiles: index.stats.totalFiles,
|
|
273
307
|
lastUpdated: index.lastUpdated,
|
|
274
308
|
semanticEnabled: hasEmbeddings,
|
|
275
309
|
},
|
|
276
|
-
estimatedTokensSaved:
|
|
310
|
+
estimatedTokensSaved: 0,
|
|
277
311
|
recommendation: editScope.length > 0
|
|
278
312
|
? `Read ONLY these ${editScope.length} file(s). Do NOT scan the whole repo.`
|
|
279
313
|
: 'No strong matches — refine query or run rigour_index --semantic',
|
|
280
314
|
};
|
|
315
|
+
// Compare compact guidance with the raw contents of the selected files only.
|
|
316
|
+
// Repository-wide files excluded by structural matching are reported separately.
|
|
317
|
+
let resultText = JSON.stringify(scopePayload, null, 2);
|
|
318
|
+
scopePayload.estimatedTokensSaved = Math.max(0, fullFileTokens - estimateTokenCount(resultText));
|
|
319
|
+
resultText = JSON.stringify(scopePayload, null, 2);
|
|
281
320
|
await setSemanticQueryCache(query, commitSha, {
|
|
282
321
|
query,
|
|
283
322
|
resolvedOwner: editScope[0] ? path.dirname(editScope[0]) : 'unknown',
|
|
@@ -286,8 +325,11 @@ export async function handleContextScope(cwd, query, limit = 10, taskId, agentId
|
|
|
286
325
|
evidence: signatures.slice(0, 5),
|
|
287
326
|
commitSha,
|
|
288
327
|
confidence: matched.length > 0 ? 0.85 : 0.3,
|
|
328
|
+
sourceTokens: fullFileTokens,
|
|
329
|
+
candidateFiles: index.stats.totalFiles,
|
|
330
|
+
returnedFiles: editScope.length,
|
|
331
|
+
guidance,
|
|
289
332
|
}, cwd);
|
|
290
|
-
const resultText = JSON.stringify(scopePayload, null, 2);
|
|
291
333
|
const telemetry = scopeTelemetryMeta({
|
|
292
334
|
candidateText: fullIndexCandidate,
|
|
293
335
|
returnedText: resultText,
|
|
@@ -296,6 +338,10 @@ export async function handleContextScope(cwd, query, limit = 10, taskId, agentId
|
|
|
296
338
|
query,
|
|
297
339
|
taskId,
|
|
298
340
|
agentId,
|
|
341
|
+
candidateTokens: fullFileTokens,
|
|
342
|
+
returnedTokens: estimateTokenCount(resultText),
|
|
343
|
+
candidateFiles: index.stats.totalFiles,
|
|
344
|
+
returnedFiles: editScope.length,
|
|
299
345
|
});
|
|
300
346
|
return {
|
|
301
347
|
content: [{
|
|
@@ -303,5 +349,6 @@ export async function handleContextScope(cwd, query, limit = 10, taskId, agentId
|
|
|
303
349
|
text: appendContextFooter(`CONTEXT SCOPE\n\n${resultText}`, telemetry, 'rigour_check_pattern before creating code'),
|
|
304
350
|
}],
|
|
305
351
|
_telemetry: telemetry,
|
|
352
|
+
_guidance: guidance,
|
|
306
353
|
};
|
|
307
354
|
}
|
|
@@ -8,5 +8,17 @@ type ToolResult = {
|
|
|
8
8
|
isError?: boolean;
|
|
9
9
|
};
|
|
10
10
|
export declare function handleRun(cwd: string, command: string, requestId: string): Promise<ToolResult>;
|
|
11
|
-
export declare function handleRunSupervised(
|
|
11
|
+
export declare function handleRunSupervised(opts: {
|
|
12
|
+
runner: GateRunner;
|
|
13
|
+
cwd: string;
|
|
14
|
+
command: string;
|
|
15
|
+
maxRetries: number;
|
|
16
|
+
dryRun: boolean;
|
|
17
|
+
requestId: string;
|
|
18
|
+
config?: Config;
|
|
19
|
+
}): Promise<ToolResult>;
|
|
20
|
+
/**
|
|
21
|
+
* Fail-closed: timeout with no human decision → timeout-deny (not approve).
|
|
22
|
+
*/
|
|
23
|
+
export declare function pollArbitration(cwd: string, rid: string, timeout: number): Promise<string | null>;
|
|
12
24
|
export {};
|
|
@@ -2,35 +2,76 @@
|
|
|
2
2
|
* Execution & Supervision Tool Handlers
|
|
3
3
|
*
|
|
4
4
|
* Handlers for: rigour_run, rigour_run_supervised
|
|
5
|
-
*
|
|
6
|
-
* @since v2.17.0 — extracted from monolithic index.ts
|
|
5
|
+
* Agent Transaction Firewall: fail-closed arbitration + typed commands.
|
|
7
6
|
*/
|
|
8
7
|
import fs from "fs-extra";
|
|
9
8
|
import path from "path";
|
|
10
|
-
import { FixPacketService } from "@rigour-labs/core";
|
|
9
|
+
import { FixPacketService, runTypedCommand, evaluateTypedCommand, issueArbitrationToken, } from "@rigour-labs/core";
|
|
11
10
|
import { logStudioEvent } from '../utils/config.js';
|
|
12
11
|
import { notifyProgress } from '../utils/notifications.js';
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
async function requireHumanApproval(cwd, command, requestId, tool) {
|
|
13
|
+
const arbitrationToken = await issueArbitrationToken(cwd, requestId, 60_000);
|
|
15
14
|
await logStudioEvent(cwd, {
|
|
16
15
|
type: "interception_requested",
|
|
17
16
|
requestId,
|
|
18
|
-
tool
|
|
17
|
+
tool,
|
|
19
18
|
command,
|
|
19
|
+
arbitrationToken,
|
|
20
20
|
});
|
|
21
|
-
// 2. Poll for Human Arbitration (Max 60s wait)
|
|
22
21
|
console.error(`[RIGOUR] Waiting for human arbitration for command: ${command}`);
|
|
23
22
|
const decision = await pollArbitration(cwd, requestId, 60000);
|
|
24
|
-
if (decision === '
|
|
23
|
+
if (decision === 'approve')
|
|
24
|
+
return null;
|
|
25
|
+
await logStudioEvent(cwd, {
|
|
26
|
+
type: "firewall_deny",
|
|
27
|
+
requestId,
|
|
28
|
+
tool: "human_arbitration",
|
|
29
|
+
decision: decision ?? 'timeout-deny',
|
|
30
|
+
reason: decision === 'reject' ? 'Human rejected' : 'Arbitration timed out (fail-closed)',
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
content: [{
|
|
34
|
+
type: "text",
|
|
35
|
+
text: decision === 'reject'
|
|
36
|
+
? `❌ COMMAND REJECTED BY GOVERNOR: The execution of "${command}" was blocked by a human operator in the Governance Studio.`
|
|
37
|
+
: `❌ COMMAND DENIED (fail-closed): No human arbitration within 60s for "${command}".`,
|
|
38
|
+
}],
|
|
39
|
+
isError: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export async function handleRun(cwd, command, requestId) {
|
|
43
|
+
const typed = evaluateTypedCommand(command);
|
|
44
|
+
await logStudioEvent(cwd, {
|
|
45
|
+
type: "firewall_precheck",
|
|
46
|
+
requestId,
|
|
47
|
+
tool: "rigour_run",
|
|
48
|
+
command,
|
|
49
|
+
firewallDecision: typed.decision,
|
|
50
|
+
firewallReason: typed.reason,
|
|
51
|
+
ruleId: typed.ruleId,
|
|
52
|
+
});
|
|
53
|
+
if (typed.decision !== 'allow') {
|
|
54
|
+
await logStudioEvent(cwd, {
|
|
55
|
+
type: "firewall_deny",
|
|
56
|
+
requestId,
|
|
57
|
+
tool: "rigour_run",
|
|
58
|
+
decision: typed.decision,
|
|
59
|
+
reason: typed.reason,
|
|
60
|
+
ruleId: typed.ruleId,
|
|
61
|
+
});
|
|
25
62
|
return {
|
|
26
|
-
content: [{
|
|
63
|
+
content: [{
|
|
64
|
+
type: "text",
|
|
65
|
+
text: `❌ FIREWALL DENY (${typed.ruleId}): ${typed.reason}\nCommand: ${command}`,
|
|
66
|
+
}],
|
|
27
67
|
isError: true,
|
|
28
68
|
};
|
|
29
69
|
}
|
|
30
|
-
|
|
31
|
-
|
|
70
|
+
const denied = await requireHumanApproval(cwd, command, requestId, "rigour_run");
|
|
71
|
+
if (denied)
|
|
72
|
+
return denied;
|
|
32
73
|
try {
|
|
33
|
-
const { stdout, stderr } = await
|
|
74
|
+
const { stdout, stderr } = await runTypedCommand(command, cwd);
|
|
34
75
|
return {
|
|
35
76
|
content: [{ type: "text", text: `✅ COMMAND EXECUTED (Approved by Governor):\n\nSTDOUT:\n${stdout}\n\nSTDERR:\n${stderr}` }],
|
|
36
77
|
};
|
|
@@ -42,8 +83,29 @@ export async function handleRun(cwd, command, requestId) {
|
|
|
42
83
|
};
|
|
43
84
|
}
|
|
44
85
|
}
|
|
45
|
-
export async function handleRunSupervised(
|
|
46
|
-
const {
|
|
86
|
+
export async function handleRunSupervised(opts) {
|
|
87
|
+
const { runner, cwd, command, maxRetries, dryRun, requestId, config } = opts;
|
|
88
|
+
const typed = evaluateTypedCommand(command);
|
|
89
|
+
if (typed.decision !== 'allow') {
|
|
90
|
+
await logStudioEvent(cwd, {
|
|
91
|
+
type: "firewall_deny",
|
|
92
|
+
requestId,
|
|
93
|
+
tool: "rigour_run_supervised",
|
|
94
|
+
decision: typed.decision,
|
|
95
|
+
reason: typed.reason,
|
|
96
|
+
ruleId: typed.ruleId,
|
|
97
|
+
});
|
|
98
|
+
return {
|
|
99
|
+
content: [{
|
|
100
|
+
type: "text",
|
|
101
|
+
text: `❌ FIREWALL DENY (${typed.ruleId}): ${typed.reason}\nCommand: ${command}`,
|
|
102
|
+
}],
|
|
103
|
+
isError: true,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const denied = await requireHumanApproval(cwd, command, requestId, "rigour_run_supervised");
|
|
107
|
+
if (denied)
|
|
108
|
+
return denied;
|
|
47
109
|
let iteration = 0;
|
|
48
110
|
let lastReport = null;
|
|
49
111
|
let result = null;
|
|
@@ -61,7 +123,7 @@ export async function handleRunSupervised(runner, cwd, command, maxRetries, dryR
|
|
|
61
123
|
notifyProgress("info", `Supervisor iteration ${iteration}/${maxRetries}...`);
|
|
62
124
|
if (!dryRun) {
|
|
63
125
|
try {
|
|
64
|
-
await
|
|
126
|
+
await runTypedCommand(command, cwd);
|
|
65
127
|
}
|
|
66
128
|
catch (e) {
|
|
67
129
|
console.error(`[RIGOUR] Iteration ${iteration} command error: ${e.message}`);
|
|
@@ -99,7 +161,6 @@ export async function handleRunSupervised(runner, cwd, command, maxRetries, dryR
|
|
|
99
161
|
}
|
|
100
162
|
if (iteration >= maxRetries) {
|
|
101
163
|
notifyProgress("error", `Supervisor FAILED after ${iteration} iterations \u2014 ${lastReport.failures.length} violations remain`);
|
|
102
|
-
// Use FixPacketService for structured output instead of ad-hoc formatting
|
|
103
164
|
let fixPacketText;
|
|
104
165
|
if (config) {
|
|
105
166
|
const fixPacketService = new FixPacketService();
|
|
@@ -107,7 +168,6 @@ export async function handleRunSupervised(runner, cwd, command, maxRetries, dryR
|
|
|
107
168
|
fixPacketText = formatFixPacketForSupervisor(fixPacket);
|
|
108
169
|
}
|
|
109
170
|
else {
|
|
110
|
-
// Fallback if config not available
|
|
111
171
|
fixPacketText = lastReport.failures.map((f, i) => {
|
|
112
172
|
const sevTag = `[${(f.severity || 'medium').toUpperCase()}]`;
|
|
113
173
|
return `${i + 1}. ${sevTag} [${f.id}] ${f.title}: ${f.details}${f.files?.length ? ` (${f.files.join(', ')})` : ''}`;
|
|
@@ -168,28 +228,44 @@ function formatFixPacketForSupervisor(fixPacket) {
|
|
|
168
228
|
}
|
|
169
229
|
return lines.join('\n');
|
|
170
230
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
231
|
+
/**
|
|
232
|
+
* Fail-closed: timeout with no human decision → timeout-deny (not approve).
|
|
233
|
+
*/
|
|
234
|
+
export async function pollArbitration(cwd, rid, timeout) {
|
|
174
235
|
const eventsPath = path.join(cwd, '.rigour/events.jsonl');
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
236
|
+
const maxIterations = Math.max(1, Math.ceil(timeout / 1000));
|
|
237
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
238
|
+
const found = await readArbitrationDecision(eventsPath, rid);
|
|
239
|
+
if (found)
|
|
240
|
+
return found;
|
|
241
|
+
await sleepMs(1000);
|
|
242
|
+
}
|
|
243
|
+
return "timeout-deny";
|
|
244
|
+
}
|
|
245
|
+
async function readArbitrationDecision(eventsPath, rid) {
|
|
246
|
+
if (!(await fs.pathExists(eventsPath)))
|
|
247
|
+
return null;
|
|
248
|
+
const content = await fs.readFile(eventsPath, 'utf-8');
|
|
249
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
250
|
+
for (const line of lines.reverse()) {
|
|
251
|
+
let event;
|
|
252
|
+
try {
|
|
253
|
+
event = JSON.parse(line);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (event.tool === 'human_arbitration' && event.requestId === rid) {
|
|
259
|
+
return event.decision;
|
|
191
260
|
}
|
|
192
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
193
261
|
}
|
|
194
|
-
return
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
function sleepMs(ms) {
|
|
265
|
+
return new Promise(resolve => {
|
|
266
|
+
const timerId = setTimeout(() => {
|
|
267
|
+
clearTimeout(timerId);
|
|
268
|
+
resolve();
|
|
269
|
+
}, ms);
|
|
270
|
+
});
|
|
195
271
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @since v3.0.0 — real-time hooks for AI coding tools
|
|
8
8
|
* @since v4.2.0 — AI Agent DLP warning layer
|
|
9
9
|
*/
|
|
10
|
-
import { runHookChecker, generateHookFiles } from "@rigour-labs/core";
|
|
10
|
+
import { runHookChecker, generateHookFiles, updateAutomaticIndexForFiles } from "@rigour-labs/core";
|
|
11
11
|
import { scanInputForCredentials, formatDLPAlert, createDLPAuditEntry, generateDLPHookFiles } from "@rigour-labs/core";
|
|
12
12
|
import fs from "fs-extra";
|
|
13
13
|
import path from "path";
|
|
@@ -73,7 +73,12 @@ export async function handleHooksCheck(cwd, files, timeout, text, agent) {
|
|
|
73
73
|
const input = { cwd, files };
|
|
74
74
|
if (timeout)
|
|
75
75
|
input.timeout_ms = timeout;
|
|
76
|
+
if (agent)
|
|
77
|
+
input.agentId = agent;
|
|
76
78
|
const result = await runHookChecker(input);
|
|
79
|
+
if (files.length > 0) {
|
|
80
|
+
await updateAutomaticIndexForFiles(cwd, files).catch(() => undefined);
|
|
81
|
+
}
|
|
77
82
|
if (result.status === 'pass') {
|
|
78
83
|
return {
|
|
79
84
|
content: [{
|
|
@@ -52,7 +52,7 @@ export async function handleIndex(cwd, options = {}) {
|
|
|
52
52
|
const candidateEstimate = 'Full codebase AST scan for pattern extraction';
|
|
53
53
|
try {
|
|
54
54
|
notifyProgress('info', 'Building pattern index...');
|
|
55
|
-
const indexer = new PatternIndexer(cwd, { useEmbeddings: options.semantic ??
|
|
55
|
+
const indexer = new PatternIndexer(cwd, { useEmbeddings: options.semantic ?? true });
|
|
56
56
|
const existingIndex = await loadPatternIndex(indexPath);
|
|
57
57
|
let index;
|
|
58
58
|
if (existingIndex && !options.force) {
|
|
@@ -60,7 +60,7 @@ async function getIndexHealthBlock(cwd) {
|
|
|
60
60
|
}
|
|
61
61
|
return `\n\n📊 Pattern Index: ${index.stats.totalPatterns} patterns across ${index.stats.totalFiles} files (updated ${index.lastUpdated}).`;
|
|
62
62
|
}
|
|
63
|
-
function wrapRecallResult(text, candidateText, cacheStatus, deduplicatedTokens = 0) {
|
|
63
|
+
function wrapRecallResult(text, candidateText, cacheStatus, deduplicatedTokens = 0, guidance) {
|
|
64
64
|
const telemetry = buildTelemetryMeta({
|
|
65
65
|
candidateText,
|
|
66
66
|
returnedText: text,
|
|
@@ -73,6 +73,7 @@ function wrapRecallResult(text, candidateText, cacheStatus, deduplicatedTokens =
|
|
|
73
73
|
text: appendContextFooter(text, telemetry, 'rigour_context_scope("your task")'),
|
|
74
74
|
}],
|
|
75
75
|
_telemetry: telemetry,
|
|
76
|
+
_guidance: guidance,
|
|
76
77
|
};
|
|
77
78
|
}
|
|
78
79
|
export async function handleRemember(cwd, key, value) {
|
|
@@ -137,7 +138,12 @@ export async function handleRecall(cwd, key) {
|
|
|
137
138
|
if (cached?.evidence?.length) {
|
|
138
139
|
const cachedBody = cached.evidence.join('\n');
|
|
139
140
|
const indexHealth = await getIndexHealthBlock(cwd);
|
|
140
|
-
return wrapRecallResult(`${cachedBody}${indexHealth}`, candidateText, 'semantic-hit', Math.max(0, estimateTokenCount(candidateText) - estimateTokenCount(cachedBody))
|
|
141
|
+
return wrapRecallResult(`${cachedBody}${indexHealth}`, candidateText, 'semantic-hit', Math.max(0, estimateTokenCount(candidateText) - estimateTokenCount(cachedBody)), cached.guidance ?? {
|
|
142
|
+
kind: 'memory',
|
|
143
|
+
query: key ?? 'all memories',
|
|
144
|
+
recommendation: key ? `Apply recalled memory "${key}" where relevant.` : 'Apply the recalled project memories where relevant.',
|
|
145
|
+
memoryRefs: key ? [{ id: key, label: key }] : [],
|
|
146
|
+
});
|
|
141
147
|
}
|
|
142
148
|
if (key) {
|
|
143
149
|
const memory = store.memories[key];
|
|
@@ -164,6 +170,12 @@ export async function handleRecall(cwd, key) {
|
|
|
164
170
|
const body = `RECALLED MEMORY [${key}]:\n${memory.value}\n\n(Stored: ${memory.timestamp})`;
|
|
165
171
|
const indexHealth = await getIndexHealthBlock(cwd);
|
|
166
172
|
const fullText = `${body}${indexHealth}`;
|
|
173
|
+
const guidance = {
|
|
174
|
+
kind: 'memory',
|
|
175
|
+
query: key,
|
|
176
|
+
recommendation: `Apply recalled memory "${key}" where relevant.`,
|
|
177
|
+
memoryRefs: [{ id: key, label: key }],
|
|
178
|
+
};
|
|
167
179
|
await setSemanticQueryCache(cacheQuery, commitSha, {
|
|
168
180
|
query: cacheQuery,
|
|
169
181
|
resolvedOwner: 'memory',
|
|
@@ -172,8 +184,9 @@ export async function handleRecall(cwd, key) {
|
|
|
172
184
|
evidence: [body],
|
|
173
185
|
commitSha,
|
|
174
186
|
confidence: 1,
|
|
187
|
+
guidance,
|
|
175
188
|
}, cwd);
|
|
176
|
-
return wrapRecallResult(fullText, candidateText, 'miss');
|
|
189
|
+
return wrapRecallResult(fullText, candidateText, 'miss', 0, guidance);
|
|
177
190
|
}
|
|
178
191
|
const keys = Object.keys(store.memories);
|
|
179
192
|
if (keys.length === 0) {
|
|
@@ -207,6 +220,12 @@ export async function handleRecall(cwd, key) {
|
|
|
207
220
|
text = "NO MEMORIES STORED. Use rigour_remember to persist important instructions.";
|
|
208
221
|
}
|
|
209
222
|
text += await getIndexHealthBlock(cwd);
|
|
223
|
+
const guidance = {
|
|
224
|
+
kind: 'memory',
|
|
225
|
+
query: 'all memories',
|
|
226
|
+
recommendation: `Apply ${cleanMemories.length} recalled project memory item(s) where relevant.`,
|
|
227
|
+
memoryRefs: keys.filter(k => !taintedKeys.includes(k)).map(k => ({ id: k, label: k })),
|
|
228
|
+
};
|
|
210
229
|
await setSemanticQueryCache(cacheQuery, commitSha, {
|
|
211
230
|
query: cacheQuery,
|
|
212
231
|
resolvedOwner: 'memory',
|
|
@@ -215,8 +234,9 @@ export async function handleRecall(cwd, key) {
|
|
|
215
234
|
evidence: [text],
|
|
216
235
|
commitSha,
|
|
217
236
|
confidence: 1,
|
|
237
|
+
guidance,
|
|
218
238
|
}, cwd);
|
|
219
|
-
return wrapRecallResult(text, candidateText, 'miss', Math.max(0, estimateTokenCount(candidateText) - estimateTokenCount(text)));
|
|
239
|
+
return wrapRecallResult(text, candidateText, 'miss', Math.max(0, estimateTokenCount(candidateText) - estimateTokenCount(text)), guidance);
|
|
220
240
|
}
|
|
221
241
|
export async function handleForget(cwd, key) {
|
|
222
242
|
const store = await loadMemory(cwd);
|
|
@@ -68,9 +68,15 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
|
|
|
68
68
|
text: appendContextFooter(cachedText, telemetry, 'proceed with implementation or rigour_check when done'),
|
|
69
69
|
}],
|
|
70
70
|
_telemetry: telemetry,
|
|
71
|
+
_guidance: cached.guidance ?? {
|
|
72
|
+
kind: 'pattern',
|
|
73
|
+
query: patternName,
|
|
74
|
+
recommendation: `Apply the cached pattern guidance for "${patternName}".`,
|
|
75
|
+
},
|
|
71
76
|
};
|
|
72
77
|
}
|
|
73
78
|
let resultText = "";
|
|
79
|
+
let matchedPattern;
|
|
74
80
|
// 0. File Guard — BLOCK writes to protected paths
|
|
75
81
|
if (file) {
|
|
76
82
|
const matched = await checkFileGuard(cwd, file);
|
|
@@ -88,6 +94,12 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
|
|
|
88
94
|
return {
|
|
89
95
|
content: [{ type: "text", text: appendContextFooter(resultText, telemetry) }],
|
|
90
96
|
_telemetry: telemetry,
|
|
97
|
+
_guidance: {
|
|
98
|
+
kind: 'pattern',
|
|
99
|
+
query: patternName,
|
|
100
|
+
recommendation: `STOP. Do not modify protected path "${file}" without human review.`,
|
|
101
|
+
selectedFiles: [file],
|
|
102
|
+
},
|
|
91
103
|
};
|
|
92
104
|
}
|
|
93
105
|
}
|
|
@@ -96,6 +108,7 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
|
|
|
96
108
|
const matcher = new PatternMatcher(index);
|
|
97
109
|
const matchResult = await matcher.match({ name: patternName, type, intent });
|
|
98
110
|
if (matchResult.status === "FOUND_SIMILAR") {
|
|
111
|
+
matchedPattern = matchResult.matches[0].pattern;
|
|
99
112
|
resultText += `🚨 PATTERN REINVENTION DETECTED\n`;
|
|
100
113
|
resultText += `Similar pattern already exists: "${matchResult.matches[0].pattern.name}" in ${matchResult.matches[0].pattern.file}\n`;
|
|
101
114
|
resultText += `SUGGESTION: ${matchResult.suggestion}\n\n`;
|
|
@@ -128,11 +141,12 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
|
|
|
128
141
|
resultText += `\n`;
|
|
129
142
|
}
|
|
130
143
|
}
|
|
144
|
+
let recommendation = 'Proceed with implementation; no conflicting codebase pattern was found.';
|
|
131
145
|
if (!resultText) {
|
|
132
146
|
resultText = `✅ Pattern "${patternName}" is fresh, secure, and unique to the codebase.\n\nRECOMMENDED ACTION: Proceed with implementation.`;
|
|
133
147
|
}
|
|
134
148
|
else {
|
|
135
|
-
|
|
149
|
+
recommendation = "Proceed with caution, addressing the warnings above.";
|
|
136
150
|
if (resultText.includes("🚨 PATTERN REINVENTION")) {
|
|
137
151
|
recommendation = "STOP and REUSE the existing pattern mentioned above. Do not create a duplicate.";
|
|
138
152
|
}
|
|
@@ -144,6 +158,13 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
|
|
|
144
158
|
}
|
|
145
159
|
resultText += `\nRECOMMENDED ACTION: ${recommendation}`;
|
|
146
160
|
}
|
|
161
|
+
const guidance = {
|
|
162
|
+
kind: 'pattern',
|
|
163
|
+
query: patternName,
|
|
164
|
+
recommendation,
|
|
165
|
+
patternRefs: matchedPattern ? [{ id: matchedPattern.id, label: matchedPattern.name, file: matchedPattern.file }] : [],
|
|
166
|
+
selectedFiles: file ? [file] : matchedPattern?.file ? [matchedPattern.file] : [],
|
|
167
|
+
};
|
|
147
168
|
await setSemanticQueryCache(cacheQuery, commitSha, {
|
|
148
169
|
query: cacheQuery,
|
|
149
170
|
resolvedOwner: file ? path.dirname(file) : 'patterns',
|
|
@@ -152,6 +173,7 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
|
|
|
152
173
|
evidence: [resultText],
|
|
153
174
|
commitSha,
|
|
154
175
|
confidence: resultText.includes('✅') ? 0.9 : 0.7,
|
|
176
|
+
guidance,
|
|
155
177
|
}, cwd);
|
|
156
178
|
const telemetry = buildTelemetryMeta({
|
|
157
179
|
candidateText: indexScanEstimate,
|
|
@@ -167,6 +189,7 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
|
|
|
167
189
|
text: appendContextFooter(resultText, telemetry, 'rigour_check before declaring done'),
|
|
168
190
|
}],
|
|
169
191
|
_telemetry: telemetry,
|
|
192
|
+
_guidance: guidance,
|
|
170
193
|
};
|
|
171
194
|
}
|
|
172
195
|
export async function handleSecurityAudit(cwd) {
|
|
@@ -8,8 +8,32 @@ export type TelemetryMeta = {
|
|
|
8
8
|
taskId?: string;
|
|
9
9
|
agentId?: string;
|
|
10
10
|
queryHash?: string;
|
|
11
|
+
candidateFiles?: number;
|
|
12
|
+
returnedFiles?: number;
|
|
11
13
|
skipRecording?: boolean;
|
|
12
14
|
};
|
|
15
|
+
export type GuidanceMeta = {
|
|
16
|
+
kind: 'context-scope' | 'pattern' | 'memory';
|
|
17
|
+
query?: string;
|
|
18
|
+
recommendation: string;
|
|
19
|
+
patternRefs?: Array<{
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
file?: string;
|
|
23
|
+
}>;
|
|
24
|
+
lessonRefs?: Array<{
|
|
25
|
+
id: string;
|
|
26
|
+
label: string;
|
|
27
|
+
visibility?: string;
|
|
28
|
+
}>;
|
|
29
|
+
memoryRefs?: Array<{
|
|
30
|
+
id: string;
|
|
31
|
+
label: string;
|
|
32
|
+
}>;
|
|
33
|
+
selectedFiles?: string[];
|
|
34
|
+
excludedFileCount?: number;
|
|
35
|
+
conflicts?: number;
|
|
36
|
+
};
|
|
13
37
|
export type ToolResult = {
|
|
14
38
|
content: {
|
|
15
39
|
type: string;
|
|
@@ -20,6 +44,7 @@ export type ToolResult = {
|
|
|
20
44
|
_rigour_report?: unknown;
|
|
21
45
|
_meta?: unknown;
|
|
22
46
|
_telemetry?: TelemetryMeta;
|
|
47
|
+
_guidance?: GuidanceMeta;
|
|
23
48
|
};
|
|
24
49
|
export declare function buildTelemetryMeta(opts: {
|
|
25
50
|
candidateText: string;
|
|
@@ -29,6 +54,10 @@ export declare function buildTelemetryMeta(opts: {
|
|
|
29
54
|
taskId?: string;
|
|
30
55
|
agentId?: string;
|
|
31
56
|
queryHash?: string;
|
|
57
|
+
candidateTokens?: number;
|
|
58
|
+
returnedTokens?: number;
|
|
59
|
+
candidateFiles?: number;
|
|
60
|
+
returnedFiles?: number;
|
|
32
61
|
skipRecording?: boolean;
|
|
33
62
|
}): TelemetryMeta;
|
|
34
63
|
/** Resolve a short commit/ref fingerprint for cache keys. */
|
|
@@ -6,14 +6,16 @@ import path from 'path';
|
|
|
6
6
|
import { estimateTokenCount } from '@rigour-labs/core';
|
|
7
7
|
export function buildTelemetryMeta(opts) {
|
|
8
8
|
return {
|
|
9
|
-
candidateTokens: estimateTokenCount(opts.candidateText),
|
|
10
|
-
returnedTokens: estimateTokenCount(opts.returnedText),
|
|
9
|
+
candidateTokens: opts.candidateTokens ?? estimateTokenCount(opts.candidateText),
|
|
10
|
+
returnedTokens: opts.returnedTokens ?? estimateTokenCount(opts.returnedText),
|
|
11
11
|
cacheStatus: opts.cacheStatus,
|
|
12
12
|
deduplicatedTokens: opts.deduplicatedTokens ?? 0,
|
|
13
13
|
isEstimated: true,
|
|
14
14
|
taskId: opts.taskId,
|
|
15
15
|
agentId: opts.agentId,
|
|
16
16
|
queryHash: opts.queryHash,
|
|
17
|
+
candidateFiles: opts.candidateFiles,
|
|
18
|
+
returnedFiles: opts.returnedFiles,
|
|
17
19
|
skipRecording: opts.skipRecording,
|
|
18
20
|
};
|
|
19
21
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { GuidanceMeta, TelemetryMeta } from './context-telemetry.js';
|
|
2
|
+
export declare function buildStudioImpact(guidance?: GuidanceMeta, telemetry?: TelemetryMeta): {
|
|
3
|
+
guidance: GuidanceMeta | undefined;
|
|
4
|
+
telemetry: {
|
|
5
|
+
candidateTokens: number;
|
|
6
|
+
returnedTokens: number;
|
|
7
|
+
candidateFiles: number | undefined;
|
|
8
|
+
returnedFiles: number | undefined;
|
|
9
|
+
cacheStatus: import("./context-telemetry.js").CacheStatus;
|
|
10
|
+
classification: "observed" | "measured estimate";
|
|
11
|
+
} | undefined;
|
|
12
|
+
};
|
|
13
|
+
export declare function buildMcpResultMeta(existingMeta: unknown, requestId: string, resourceUri: string, guidance?: GuidanceMeta, telemetry?: TelemetryMeta): Record<string, unknown>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
function classification(telemetry) {
|
|
2
|
+
return telemetry?.isEstimated ? 'measured estimate' : 'observed';
|
|
3
|
+
}
|
|
4
|
+
export function buildStudioImpact(guidance, telemetry) {
|
|
5
|
+
return {
|
|
6
|
+
guidance,
|
|
7
|
+
telemetry: telemetry ? {
|
|
8
|
+
candidateTokens: telemetry.candidateTokens,
|
|
9
|
+
returnedTokens: telemetry.returnedTokens,
|
|
10
|
+
candidateFiles: telemetry.candidateFiles,
|
|
11
|
+
returnedFiles: telemetry.returnedFiles,
|
|
12
|
+
cacheStatus: telemetry.cacheStatus,
|
|
13
|
+
classification: classification(telemetry),
|
|
14
|
+
} : undefined,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function buildMcpResultMeta(existingMeta, requestId, resourceUri, guidance, telemetry) {
|
|
18
|
+
const base = existingMeta && typeof existingMeta === 'object' && !Array.isArray(existingMeta)
|
|
19
|
+
? existingMeta
|
|
20
|
+
: {};
|
|
21
|
+
if (!guidance)
|
|
22
|
+
return { ...base, ui: { resourceUri } };
|
|
23
|
+
return {
|
|
24
|
+
...base,
|
|
25
|
+
ui: { resourceUri },
|
|
26
|
+
rigourImpact: {
|
|
27
|
+
requestId,
|
|
28
|
+
classification: classification(telemetry),
|
|
29
|
+
guidance,
|
|
30
|
+
context: telemetry ? {
|
|
31
|
+
candidateTokens: telemetry.candidateTokens,
|
|
32
|
+
returnedTokens: telemetry.returnedTokens,
|
|
33
|
+
avoidedTokens: Math.max(0, telemetry.candidateTokens - telemetry.returnedTokens),
|
|
34
|
+
candidateFiles: telemetry.candidateFiles,
|
|
35
|
+
returnedFiles: telemetry.returnedFiles,
|
|
36
|
+
excludedFiles: typeof telemetry.candidateFiles === 'number' && typeof telemetry.returnedFiles === 'number'
|
|
37
|
+
? Math.max(0, telemetry.candidateFiles - telemetry.returnedFiles)
|
|
38
|
+
: undefined,
|
|
39
|
+
cacheStatus: telemetry.cacheStatus,
|
|
40
|
+
} : undefined,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.1.0-beta.1",
|
|
4
4
|
"description": "MCP server + live dashboard for AI code governance — OWASP LLM Top 10 (10/10), real-time MCP App UI, 25+ security patterns, Bayesian learning Brain, hallucinated import detection, multi-agent governance. Works with Claude, Cursor, VS Code, ChatGPT, Goose, Windsurf. Industry presets for HIPAA, SOC2, FedRAMP.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://rigour.run",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"execa": "^8.0.1",
|
|
63
63
|
"fs-extra": "^11.4.0",
|
|
64
64
|
"yaml": "^2.8.2",
|
|
65
|
-
"@rigour-labs/core": "
|
|
65
|
+
"@rigour-labs/core": "6.1.0-beta.1"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@types/node": "^25.0.3",
|