@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(runner, cwd, command, maxRetries, dryRun, requestId, config);
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(runner: GateRunner, cwd: string, command: string, maxRetries: number, dryRun: boolean, requestId: string, config?: Config): Promise<ToolResult>;
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
- export async function handleRun(cwd, command, requestId) {
14
- // 1. Log Interceptable Event
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: "rigour_run",
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 === 'reject') {
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: [{ type: "text", text: `❌ COMMAND REJECTED BY GOVERNOR: The execution of "${command}" was blocked by a human operator in the Governance Studio.` }],
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
- // Execute
31
- const { execa } = await import("execa");
70
+ const denied = await requireHumanApproval(cwd, command, requestId, "rigour_run");
71
+ if (denied)
72
+ return denied;
32
73
  try {
33
- const { stdout, stderr } = await execa(command, { shell: true, cwd });
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(runner, cwd, command, maxRetries, dryRun, requestId, config) {
46
- const { execa } = await import("execa");
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 execa(command, { shell: true, cwd });
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
- // ─── Private Helpers ──────────────────────────────────────────────
172
- async function pollArbitration(cwd, rid, timeout) {
173
- const start = Date.now();
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
- while (Date.now() - start < timeout) {
176
- if (await fs.pathExists(eventsPath)) {
177
- const content = await fs.readFile(eventsPath, 'utf-8');
178
- const lines = content.split('\n').filter(l => l.trim());
179
- for (const line of lines.reverse()) {
180
- let event;
181
- try {
182
- event = JSON.parse(line);
183
- }
184
- catch {
185
- continue;
186
- }
187
- if (event.tool === 'human_arbitration' && event.requestId === rid) {
188
- return event.decision;
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 "approve"; // Default auto-approve if no human response
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": "5.5.3",
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": "5.5.3"
65
+ "@rigour-labs/core": "6.0.0"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@types/node": "^25.0.3",