@rigour-labs/mcp 5.5.3 → 6.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/dist/index.js
CHANGED
|
@@ -136,7 +136,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
136
136
|
break;
|
|
137
137
|
case "rigour_run_supervised": {
|
|
138
138
|
const { command, maxRetries = 3, dryRun = false } = args;
|
|
139
|
-
result = await handleRunSupervised(
|
|
139
|
+
result = await handleRunSupervised({
|
|
140
|
+
runner,
|
|
141
|
+
cwd,
|
|
142
|
+
command,
|
|
143
|
+
maxRetries,
|
|
144
|
+
dryRun,
|
|
145
|
+
requestId,
|
|
146
|
+
config,
|
|
147
|
+
});
|
|
140
148
|
break;
|
|
141
149
|
}
|
|
142
150
|
// Multi-agent governance
|
|
@@ -82,6 +82,46 @@ async function validateTaskScopeAgainstIndex(cwd, taskScope) {
|
|
|
82
82
|
}
|
|
83
83
|
// ─── Agent Register ───────────────────────────────────────────────
|
|
84
84
|
export async function handleAgentRegister(cwd, agentId, taskScope, requestId) {
|
|
85
|
+
const { validateAgentClaimedScope, loadOperatorScopes, isScopeSubset, } = await import('@rigour-labs/core');
|
|
86
|
+
const authority = process.env.RIGOUR_ALLOW_AGENT_SCOPE_AUTHORITY === '1';
|
|
87
|
+
if (!authority) {
|
|
88
|
+
const claimCheck = validateAgentClaimedScope(taskScope);
|
|
89
|
+
if (!claimCheck.ok) {
|
|
90
|
+
await logStudioEvent(cwd, {
|
|
91
|
+
type: 'firewall_deny',
|
|
92
|
+
requestId,
|
|
93
|
+
tool: 'rigour_agent_register',
|
|
94
|
+
decision: 'deny',
|
|
95
|
+
reason: claimCheck.reason,
|
|
96
|
+
ruleId: 'scope.register-denied',
|
|
97
|
+
});
|
|
98
|
+
return {
|
|
99
|
+
content: [{ type: 'text', text: `❌ SCOPE REGISTER DENIED: ${claimCheck.reason}` }],
|
|
100
|
+
isError: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const operatorScopes = await loadOperatorScopes(cwd);
|
|
105
|
+
if (operatorScopes) {
|
|
106
|
+
const allowed = operatorScopes[agentId];
|
|
107
|
+
if (!allowed || !isScopeSubset(taskScope, allowed)) {
|
|
108
|
+
const msg = allowed
|
|
109
|
+
? `Claimed scope is not a subset of operator scopes for "${agentId}": ${allowed.join(', ')}`
|
|
110
|
+
: `No operator scopes defined for agent "${agentId}" in .rigour/operator-scopes.json`;
|
|
111
|
+
await logStudioEvent(cwd, {
|
|
112
|
+
type: 'firewall_deny',
|
|
113
|
+
requestId,
|
|
114
|
+
tool: 'rigour_agent_register',
|
|
115
|
+
decision: 'deny',
|
|
116
|
+
reason: msg,
|
|
117
|
+
ruleId: 'scope.operator-required',
|
|
118
|
+
});
|
|
119
|
+
return {
|
|
120
|
+
content: [{ type: 'text', text: `❌ SCOPE REGISTER DENIED: ${msg}` }],
|
|
121
|
+
isError: true,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
85
125
|
const sessionPath = path.join(cwd, '.rigour', 'agent-session.json');
|
|
86
126
|
let session = { agents: [], startedAt: new Date().toISOString() };
|
|
87
127
|
if (await fs.pathExists(sessionPath)) {
|
|
@@ -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
|
}
|
|
@@ -73,6 +73,8 @@ 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);
|
|
77
79
|
if (result.status === 'pass') {
|
|
78
80
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
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.0.0"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@types/node": "^25.0.3",
|