@rigour-labs/cli 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/cli.js +23 -0
- package/dist/commands/firewall.d.ts +9 -0
- package/dist/commands/firewall.js +106 -0
- package/dist/commands/studio.js +85 -0
- package/package.json +2 -2
- package/studio-dist/assets/index-DzNTOo-7.js +372 -0
- package/studio-dist/index.html +1 -1
- package/studio-dist/assets/index-BvR6Si_S.js +0 -362
package/dist/cli.js
CHANGED
|
@@ -19,6 +19,7 @@ import { deepStatsCommand } from './commands/deep-stats.js';
|
|
|
19
19
|
import { reviewCommand } from './commands/review.js';
|
|
20
20
|
import { checkPatternCommand } from './commands/check-pattern.js';
|
|
21
21
|
import { securityAuditCommand } from './commands/security-audit.js';
|
|
22
|
+
import { firewallTransactCommand, firewallAdversarialCommand, firewallAdmitCommand, firewallStatusCommand } from './commands/firewall.js';
|
|
22
23
|
import { checkForUpdates } from './utils/version.js';
|
|
23
24
|
import { getCliVersion } from './utils/cli-version.js';
|
|
24
25
|
import chalk from 'chalk';
|
|
@@ -363,6 +364,28 @@ settingsCmd
|
|
|
363
364
|
.command('path')
|
|
364
365
|
.description('Show settings file path')
|
|
365
366
|
.action(async () => { await settingsPathCommand(); });
|
|
367
|
+
const firewallCmd = program
|
|
368
|
+
.command('firewall')
|
|
369
|
+
.description('Agent Transaction Firewall — mediate, attest, and prove damage bounds');
|
|
370
|
+
firewallCmd
|
|
371
|
+
.command('status')
|
|
372
|
+
.description('Show transaction, attestation, and adversarial status')
|
|
373
|
+
.action(async () => { await firewallStatusCommand(process.cwd()); });
|
|
374
|
+
firewallCmd
|
|
375
|
+
.command('transact')
|
|
376
|
+
.description('Start a mediated transaction, verify gates, COMMIT or DISCARD')
|
|
377
|
+
.option('--agent <id>', 'Agent id for scope binding')
|
|
378
|
+
.option('--scope <globs>', 'Comma-separated allowed path globs', '**/*')
|
|
379
|
+
.option('--discard', 'Discard the current transaction worktree')
|
|
380
|
+
.action(async (options) => { await firewallTransactCommand(process.cwd(), options); });
|
|
381
|
+
firewallCmd
|
|
382
|
+
.command('adversarial')
|
|
383
|
+
.description('Replay deterministic adversarial corpus against the firewall kernel')
|
|
384
|
+
.action(async () => { await firewallAdversarialCommand(process.cwd()); });
|
|
385
|
+
firewallCmd
|
|
386
|
+
.command('admit')
|
|
387
|
+
.description('CI admission: require valid signed attestation with PASS gates')
|
|
388
|
+
.action(async () => { await firewallAdmitCommand(process.cwd()); });
|
|
366
389
|
// Check for updates before parsing (non-blocking)
|
|
367
390
|
(async () => {
|
|
368
391
|
try {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function firewallTransactCommand(cwd: string, options: {
|
|
2
|
+
agent?: string;
|
|
3
|
+
scope?: string;
|
|
4
|
+
discard?: boolean;
|
|
5
|
+
commit?: boolean;
|
|
6
|
+
}): Promise<void>;
|
|
7
|
+
export declare function firewallAdversarialCommand(cwd: string): Promise<void>;
|
|
8
|
+
export declare function firewallAdmitCommand(cwd: string): Promise<void>;
|
|
9
|
+
export declare function firewallStatusCommand(cwd: string): Promise<void>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firewall CLI — transact, adversarial replay, CI admit.
|
|
3
|
+
*/
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import yaml from 'yaml';
|
|
8
|
+
import { TransactionRunner, runAdversarialCorpus, persistAdversarialReport, createAttestation, admitForCi, loadLatestAttestation, verifyAttestation, loadCurrentTransaction, listTransactions, GateRunner, ConfigSchema, loadAgentScopesFromDisk, } from '@rigour-labs/core';
|
|
9
|
+
export async function firewallTransactCommand(cwd, options) {
|
|
10
|
+
if (options.discard) {
|
|
11
|
+
const current = await loadCurrentTransaction(cwd);
|
|
12
|
+
if (!current) {
|
|
13
|
+
console.error(chalk.red('No current transaction to discard'));
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const discarded = await TransactionRunner.fromRecord(cwd, current).discard();
|
|
17
|
+
console.log(chalk.yellow(`DISCARD ${discarded.id}`));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const registered = await loadAgentScopesFromDisk(cwd);
|
|
21
|
+
if (registered.length > 0 && !options.agent) {
|
|
22
|
+
console.error(chalk.red('Agent scopes are registered — pass --agent <id> to bind the writer (fail-closed)'));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const scope = (options.scope || '**/*').split(',').map(s => s.trim()).filter(Boolean);
|
|
26
|
+
const tx = new TransactionRunner(cwd, { agentId: options.agent, scope });
|
|
27
|
+
const started = await tx.start();
|
|
28
|
+
console.log(chalk.cyan(`START transaction ${started.id}`));
|
|
29
|
+
const gateCwd = started.worktreePath || cwd;
|
|
30
|
+
if (started.worktreePath) {
|
|
31
|
+
console.log(chalk.dim(`Worktree: ${started.worktreePath}`));
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
await tx.syncWorktreeChanges();
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
await tx.discard();
|
|
38
|
+
console.error(chalk.red(`DISCARD — scope/budget: ${e.message}`));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
const configPath = path.join(cwd, 'rigour.yml');
|
|
42
|
+
let config = ConfigSchema.parse({ version: 1 });
|
|
43
|
+
if (await fs.pathExists(configPath)) {
|
|
44
|
+
config = ConfigSchema.parse(yaml.parse(await fs.readFile(configPath, 'utf-8')));
|
|
45
|
+
}
|
|
46
|
+
const runner = new GateRunner(config);
|
|
47
|
+
const { ok, gateResults } = await tx.verify(async () => {
|
|
48
|
+
const report = await runner.run(gateCwd);
|
|
49
|
+
const failedGates = [...new Set(report.failures.map(f => f.id))];
|
|
50
|
+
return { status: report.status, failedGates, score: report.stats.score };
|
|
51
|
+
});
|
|
52
|
+
if (!ok) {
|
|
53
|
+
const discarded = await tx.discard();
|
|
54
|
+
console.log(chalk.red(`DISCARD ${discarded.id} — gates ${gateResults.status}`));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
const committed = await tx.commit();
|
|
58
|
+
const attestation = await createAttestation(cwd, {
|
|
59
|
+
transaction: committed,
|
|
60
|
+
gateResults: {
|
|
61
|
+
status: gateResults.status,
|
|
62
|
+
score: gateResults.score,
|
|
63
|
+
failedGates: gateResults.failedGates,
|
|
64
|
+
},
|
|
65
|
+
verifyRoot: gateCwd,
|
|
66
|
+
});
|
|
67
|
+
console.log(chalk.green(`COMMIT ${committed.id}`));
|
|
68
|
+
console.log(chalk.green(`Attestation ${attestation.signature.slice(0, 12)}… tree=${(attestation.treeDigest || '').slice(0, 12)} policy=${attestation.policyHash}`));
|
|
69
|
+
}
|
|
70
|
+
export async function firewallAdversarialCommand(cwd) {
|
|
71
|
+
const report = runAdversarialCorpus();
|
|
72
|
+
const out = await persistAdversarialReport(cwd, report);
|
|
73
|
+
console.log(chalk.bold(`Adversarial replay: ${report.passed} passed, ${report.failed} failed`));
|
|
74
|
+
for (const r of report.results) {
|
|
75
|
+
const mark = r.passed ? chalk.green('PASS') : chalk.red('FAIL');
|
|
76
|
+
console.log(` ${mark} ${r.caseId}: expected=${r.expected} actual=${r.actual} — ${r.reason}`);
|
|
77
|
+
if (r.suggestedRule) {
|
|
78
|
+
console.log(chalk.yellow(' suggested regression:\n') + chalk.dim(r.suggestedRule));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
console.log(chalk.dim(`Report: ${out}`));
|
|
82
|
+
if (report.failed > 0)
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
export async function firewallAdmitCommand(cwd) {
|
|
86
|
+
const result = await admitForCi(cwd);
|
|
87
|
+
if (!result.admit) {
|
|
88
|
+
console.error(chalk.red(`ADMIT DENIED: ${result.reason}`));
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
const bundle = await loadLatestAttestation(cwd);
|
|
92
|
+
const valid = bundle ? await verifyAttestation(cwd, bundle) : false;
|
|
93
|
+
console.log(chalk.green(`ADMIT OK: ${result.reason} (signature ${valid ? 'valid' : 'n/a'})`));
|
|
94
|
+
}
|
|
95
|
+
export async function firewallStatusCommand(cwd) {
|
|
96
|
+
const current = await loadCurrentTransaction(cwd);
|
|
97
|
+
const txs = await listTransactions(cwd);
|
|
98
|
+
const attestation = await loadLatestAttestation(cwd);
|
|
99
|
+
const advPath = path.join(cwd, '.rigour', 'adversarial-report.json');
|
|
100
|
+
const adv = await fs.pathExists(advPath) ? await fs.readJson(advPath) : null;
|
|
101
|
+
console.log(chalk.bold('Firewall status'));
|
|
102
|
+
console.log(` Current TX: ${current ? `${current.id} (${current.status})` : 'none'}`);
|
|
103
|
+
console.log(` Transactions: ${txs.length}`);
|
|
104
|
+
console.log(` Attestation: ${attestation ? `${attestation.transactionId} gates=${attestation.gateResults.status}` : 'none'}`);
|
|
105
|
+
console.log(` Adversarial: ${adv ? `${adv.passed} pass / ${adv.failed} fail` : 'not run'}`);
|
|
106
|
+
}
|
package/dist/commands/studio.js
CHANGED
|
@@ -719,12 +719,97 @@ async function handleApiRequest(req, res, url, ctx) {
|
|
|
719
719
|
});
|
|
720
720
|
return true;
|
|
721
721
|
}
|
|
722
|
+
if (url.pathname === '/api/firewall') {
|
|
723
|
+
try {
|
|
724
|
+
const { loadCurrentTransaction, listTransactions, loadLatestAttestation, verifyAttestation, } = await import('@rigour-labs/core');
|
|
725
|
+
const current = await loadCurrentTransaction(cwd);
|
|
726
|
+
const transactions = await listTransactions(cwd);
|
|
727
|
+
const attestation = await loadLatestAttestation(cwd);
|
|
728
|
+
const attestationValid = attestation ? await verifyAttestation(cwd, attestation) : false;
|
|
729
|
+
const advPath = path.join(cwd, '.rigour/adversarial-report.json');
|
|
730
|
+
const adversarial = await fs.pathExists(advPath) ? await fs.readJson(advPath) : null;
|
|
731
|
+
const decisionsPath = path.join(cwd, '.rigour/firewall-decisions.jsonl');
|
|
732
|
+
let decisions = [];
|
|
733
|
+
if (await fs.pathExists(decisionsPath)) {
|
|
734
|
+
const content = await fs.readFile(decisionsPath, 'utf8');
|
|
735
|
+
decisions = content
|
|
736
|
+
.split('\n')
|
|
737
|
+
.filter((l) => l.trim())
|
|
738
|
+
.slice(-100)
|
|
739
|
+
.map((l) => {
|
|
740
|
+
try {
|
|
741
|
+
return JSON.parse(l);
|
|
742
|
+
}
|
|
743
|
+
catch {
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
})
|
|
747
|
+
.filter(Boolean)
|
|
748
|
+
.reverse();
|
|
749
|
+
}
|
|
750
|
+
let recentDenies = [];
|
|
751
|
+
if (await fs.pathExists(eventsPath)) {
|
|
752
|
+
const content = await fs.readFile(eventsPath, 'utf8');
|
|
753
|
+
recentDenies = content
|
|
754
|
+
.split('\n')
|
|
755
|
+
.filter((l) => l.trim())
|
|
756
|
+
.map((l) => {
|
|
757
|
+
try {
|
|
758
|
+
return JSON.parse(l);
|
|
759
|
+
}
|
|
760
|
+
catch {
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
})
|
|
764
|
+
.filter((e) => e && (e.type === 'firewall_deny' || e.decision === 'timeout-deny' || e.decision === 'deny'))
|
|
765
|
+
.slice(-50)
|
|
766
|
+
.reverse();
|
|
767
|
+
}
|
|
768
|
+
const hooksPresent = (await fs.pathExists(path.join(cwd, '.cursor/hooks.json'))) ||
|
|
769
|
+
(await fs.pathExists(path.join(cwd, '.claude/settings.json'))) ||
|
|
770
|
+
(await fs.pathExists(path.join(cwd, '.clinerules'))) ||
|
|
771
|
+
(await fs.pathExists(path.join(cwd, '.windsurf/hooks.json')));
|
|
772
|
+
const agentScopesPath = path.join(cwd, '.rigour/agent-session.json');
|
|
773
|
+
const agentSession = await fs.pathExists(agentScopesPath) ? await fs.readJson(agentScopesPath) : null;
|
|
774
|
+
const scopeActive = Array.isArray(agentSession?.agents) && agentSession.agents.length > 0;
|
|
775
|
+
const typedSeen = recentDenies.some((e) => e.ruleId?.startsWith?.('shell.') || e.tool === 'rigour_run');
|
|
776
|
+
const gatewayWired = false; // McpGateway not yet the MCP proxy path
|
|
777
|
+
sendJson(res, 200, {
|
|
778
|
+
current,
|
|
779
|
+
transactions: transactions.slice(0, 20),
|
|
780
|
+
attestation,
|
|
781
|
+
attestationValid,
|
|
782
|
+
adversarial,
|
|
783
|
+
decisions,
|
|
784
|
+
recentDenies,
|
|
785
|
+
failClosed: true,
|
|
786
|
+
mediation: {
|
|
787
|
+
status: gatewayWired && hooksPresent ? 'full' : 'partial',
|
|
788
|
+
typedCommands: typedSeen || hooksPresent ? 'rigour_run_only' : 'not_observed',
|
|
789
|
+
scopeEnforcement: scopeActive ? 'requires_agent_id' : 'inactive',
|
|
790
|
+
arbitration: 'fail-closed',
|
|
791
|
+
hooksInstalled: hooksPresent,
|
|
792
|
+
mcpGateway: gatewayWired,
|
|
793
|
+
},
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
catch (e) {
|
|
797
|
+
sendJson(res, 500, { error: e.message });
|
|
798
|
+
}
|
|
799
|
+
return true;
|
|
800
|
+
}
|
|
722
801
|
if (url.pathname === '/api/arbitrate' && req.method === 'POST') {
|
|
723
802
|
let body = '';
|
|
724
803
|
req.on('data', (chunk) => (body += chunk));
|
|
725
804
|
req.on('end', async () => {
|
|
726
805
|
try {
|
|
727
806
|
const decision = JSON.parse(body);
|
|
807
|
+
const { consumeArbitrationToken } = await import('@rigour-labs/core');
|
|
808
|
+
const ok = await consumeArbitrationToken(cwd, decision.requestId, decision.token);
|
|
809
|
+
if (!ok) {
|
|
810
|
+
sendJson(res, 403, { error: 'Invalid or missing arbitration token (one-time, fail-closed)' });
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
728
813
|
const logEntry = JSON.stringify({
|
|
729
814
|
id: randomUUID(),
|
|
730
815
|
timestamp: new Date().toISOString(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "AI-native quality gates with local LLM analysis. Forces AI agents (Claude, Cursor, Copilot, Cline, Windsurf) to meet engineering standards. Bayesian Brain learns your codebase. Zero config: npx rigour-scan.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://rigour.run",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"inquirer": "9.2.16",
|
|
54
54
|
"ora": "^8.0.1",
|
|
55
55
|
"yaml": "^2.8.2",
|
|
56
|
-
"@rigour-labs/core": "
|
|
56
|
+
"@rigour-labs/core": "6.0.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/fs-extra": "^11.0.4",
|