@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 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
+ }
@@ -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": "5.5.3",
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": "5.5.3"
56
+ "@rigour-labs/core": "6.0.0"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@types/fs-extra": "^11.0.4",