@rigour-labs/core 5.5.4 → 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/firewall/adversarial.d.ts +13 -0
- package/dist/firewall/adversarial.js +125 -0
- package/dist/firewall/arbitration-token.d.ts +8 -0
- package/dist/firewall/arbitration-token.js +58 -0
- package/dist/firewall/attestation.d.ts +38 -0
- package/dist/firewall/attestation.js +197 -0
- package/dist/firewall/capability-broker.d.ts +47 -0
- package/dist/firewall/capability-broker.js +209 -0
- package/dist/firewall/firewall.test.d.ts +1 -0
- package/dist/firewall/firewall.test.js +102 -0
- package/dist/firewall/index.d.ts +10 -0
- package/dist/firewall/index.js +10 -0
- package/dist/firewall/mcp-gateway.d.ts +40 -0
- package/dist/firewall/mcp-gateway.js +64 -0
- package/dist/firewall/policy-hash.d.ts +1 -0
- package/dist/firewall/policy-hash.js +5 -0
- package/dist/firewall/scope-enforcement.d.ts +26 -0
- package/dist/firewall/scope-enforcement.js +155 -0
- package/dist/firewall/transaction.d.ts +43 -0
- package/dist/firewall/transaction.js +216 -0
- package/dist/firewall/typed-command.d.ts +16 -0
- package/dist/firewall/typed-command.js +129 -0
- package/dist/firewall/types.d.ts +96 -0
- package/dist/firewall/types.js +5 -0
- package/dist/hooks/checker.d.ts +2 -0
- package/dist/hooks/checker.js +25 -1
- package/dist/hooks/standalone-checker.js +5 -1
- package/dist/hooks/templates.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +6 -6
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transactional execution — worktree START → verify → COMMIT | DISCARD.
|
|
3
|
+
*/
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { execa } from 'execa';
|
|
8
|
+
import { hashPolicy } from './policy-hash.js';
|
|
9
|
+
import { assertWritesInScope } from './scope-enforcement.js';
|
|
10
|
+
const DEFAULT_BUDGETS = {
|
|
11
|
+
maxFiles: 20,
|
|
12
|
+
maxRetries: 3,
|
|
13
|
+
maxDurationMs: 30 * 60 * 1000,
|
|
14
|
+
};
|
|
15
|
+
export class TransactionRunner {
|
|
16
|
+
cwd;
|
|
17
|
+
record;
|
|
18
|
+
constructor(cwd, opts) {
|
|
19
|
+
this.cwd = cwd;
|
|
20
|
+
const budgets = { ...DEFAULT_BUDGETS, ...opts.budgets };
|
|
21
|
+
this.record = {
|
|
22
|
+
id: randomUUID(),
|
|
23
|
+
agentId: opts.agentId,
|
|
24
|
+
scope: opts.scope,
|
|
25
|
+
status: 'START',
|
|
26
|
+
budgets,
|
|
27
|
+
filesChanged: [],
|
|
28
|
+
capabilitiesIssued: [],
|
|
29
|
+
decisions: [],
|
|
30
|
+
startedAt: new Date().toISOString(),
|
|
31
|
+
policyHash: opts.policyHash ?? hashPolicy({ scope: opts.scope, budgets }),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
static fromRecord(cwd, record) {
|
|
35
|
+
const runner = new TransactionRunner(cwd, {
|
|
36
|
+
agentId: record.agentId,
|
|
37
|
+
scope: record.scope,
|
|
38
|
+
budgets: record.budgets,
|
|
39
|
+
policyHash: record.policyHash,
|
|
40
|
+
});
|
|
41
|
+
runner.record = { ...record, decisions: [...record.decisions], filesChanged: [...record.filesChanged] };
|
|
42
|
+
return runner;
|
|
43
|
+
}
|
|
44
|
+
getRecord() {
|
|
45
|
+
return { ...this.record, decisions: [...this.record.decisions], filesChanged: [...this.record.filesChanged] };
|
|
46
|
+
}
|
|
47
|
+
setStatus(status) {
|
|
48
|
+
this.record.status = status;
|
|
49
|
+
}
|
|
50
|
+
addDecision(ev) {
|
|
51
|
+
this.record.decisions.push(ev);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Create an ephemeral git worktree for isolated writes.
|
|
55
|
+
*/
|
|
56
|
+
async start() {
|
|
57
|
+
const worktreesRoot = path.join(this.cwd, '.rigour', 'worktrees');
|
|
58
|
+
await fs.ensureDir(worktreesRoot);
|
|
59
|
+
const worktreePath = path.join(worktreesRoot, this.record.id);
|
|
60
|
+
const branch = `rigour-tx-${this.record.id.slice(0, 8)}`;
|
|
61
|
+
try {
|
|
62
|
+
await execa('git', ['rev-parse', '--is-inside-work-tree'], { cwd: this.cwd });
|
|
63
|
+
await execa('git', ['worktree', 'add', '-b', branch, worktreePath, 'HEAD'], {
|
|
64
|
+
cwd: this.cwd,
|
|
65
|
+
shell: false,
|
|
66
|
+
});
|
|
67
|
+
this.record.worktreePath = worktreePath;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Fallback: copy-less sandbox dir (no git) — still tracks commit/discard via marker
|
|
71
|
+
await fs.ensureDir(worktreePath);
|
|
72
|
+
this.record.worktreePath = worktreePath;
|
|
73
|
+
await fs.writeJson(path.join(worktreePath, '.rigour-tx.json'), {
|
|
74
|
+
id: this.record.id,
|
|
75
|
+
base: this.cwd,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
this.setStatus('RUNNING');
|
|
79
|
+
await this.persist();
|
|
80
|
+
return this.getRecord();
|
|
81
|
+
}
|
|
82
|
+
async noteFileChange(relPath) {
|
|
83
|
+
const scopeEv = await assertWritesInScope(this.cwd, [relPath], this.record.agentId);
|
|
84
|
+
this.addDecision(scopeEv);
|
|
85
|
+
if (scopeEv.decision !== 'allow') {
|
|
86
|
+
return scopeEv;
|
|
87
|
+
}
|
|
88
|
+
if (!this.record.filesChanged.includes(relPath)) {
|
|
89
|
+
this.record.filesChanged.push(relPath);
|
|
90
|
+
}
|
|
91
|
+
if (this.record.filesChanged.length > this.record.budgets.maxFiles) {
|
|
92
|
+
const deny = {
|
|
93
|
+
decision: 'deny',
|
|
94
|
+
reason: `File budget exceeded (${this.record.budgets.maxFiles})`,
|
|
95
|
+
ruleId: 'budget.files',
|
|
96
|
+
policyHash: this.record.policyHash,
|
|
97
|
+
timestamp: new Date().toISOString(),
|
|
98
|
+
};
|
|
99
|
+
this.addDecision(deny);
|
|
100
|
+
return deny;
|
|
101
|
+
}
|
|
102
|
+
const elapsed = Date.now() - Date.parse(this.record.startedAt);
|
|
103
|
+
if (elapsed > this.record.budgets.maxDurationMs) {
|
|
104
|
+
const deny = {
|
|
105
|
+
decision: 'deny',
|
|
106
|
+
reason: 'Time budget exceeded',
|
|
107
|
+
ruleId: 'budget.time',
|
|
108
|
+
policyHash: this.record.policyHash,
|
|
109
|
+
timestamp: new Date().toISOString(),
|
|
110
|
+
};
|
|
111
|
+
this.addDecision(deny);
|
|
112
|
+
return deny;
|
|
113
|
+
}
|
|
114
|
+
await this.persist();
|
|
115
|
+
return scopeEv;
|
|
116
|
+
}
|
|
117
|
+
async syncWorktreeChanges() {
|
|
118
|
+
const root = this.record.worktreePath || this.cwd;
|
|
119
|
+
const files = await listChangedFiles(root);
|
|
120
|
+
for (const f of files) {
|
|
121
|
+
const ev = await this.noteFileChange(f);
|
|
122
|
+
if (ev.decision !== 'allow') {
|
|
123
|
+
throw Object.assign(new Error(ev.reason), { evaluation: ev, code: 'FIREWALL_DENY' });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return [...this.record.filesChanged];
|
|
127
|
+
}
|
|
128
|
+
getWorktreePath() {
|
|
129
|
+
return this.record.worktreePath;
|
|
130
|
+
}
|
|
131
|
+
async verify(runGates) {
|
|
132
|
+
this.setStatus('VERIFYING');
|
|
133
|
+
const gateResults = await runGates();
|
|
134
|
+
await this.persist();
|
|
135
|
+
return { ok: gateResults.status === 'PASS', gateResults };
|
|
136
|
+
}
|
|
137
|
+
async commit() {
|
|
138
|
+
this.setStatus('COMMIT');
|
|
139
|
+
this.record.finishedAt = new Date().toISOString();
|
|
140
|
+
// Worktree branch is left for operator merge; attestation binds treeDigest/commitSha
|
|
141
|
+
await this.persist();
|
|
142
|
+
return this.getRecord();
|
|
143
|
+
}
|
|
144
|
+
async discard() {
|
|
145
|
+
this.setStatus('DISCARD');
|
|
146
|
+
this.record.finishedAt = new Date().toISOString();
|
|
147
|
+
const wt = this.record.worktreePath;
|
|
148
|
+
if (wt && await fs.pathExists(wt)) {
|
|
149
|
+
try {
|
|
150
|
+
await execa('git', ['worktree', 'remove', '--force', wt], { cwd: this.cwd, shell: false });
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
await fs.remove(wt);
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const branch = `rigour-tx-${this.record.id.slice(0, 8)}`;
|
|
157
|
+
await execa('git', ['branch', '-D', branch], { cwd: this.cwd, shell: false, reject: false });
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// ignore
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
await this.persist();
|
|
164
|
+
return this.getRecord();
|
|
165
|
+
}
|
|
166
|
+
async persist() {
|
|
167
|
+
const dir = path.join(this.cwd, '.rigour', 'transactions');
|
|
168
|
+
await fs.ensureDir(dir);
|
|
169
|
+
await fs.writeJson(path.join(dir, `${this.record.id}.json`), this.record, { spaces: 2 });
|
|
170
|
+
await fs.writeJson(path.join(this.cwd, '.rigour', 'transaction-current.json'), this.record, { spaces: 2 });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
export async function listChangedFiles(dir) {
|
|
174
|
+
try {
|
|
175
|
+
const { stdout: diffOut } = await execa('git', ['diff', '--name-only', 'HEAD'], {
|
|
176
|
+
cwd: dir,
|
|
177
|
+
shell: false,
|
|
178
|
+
});
|
|
179
|
+
const { stdout: untrackedOut } = await execa('git', ['ls-files', '--others', '--exclude-standard'], { cwd: dir, shell: false });
|
|
180
|
+
const files = [
|
|
181
|
+
...diffOut.split('\n'),
|
|
182
|
+
...untrackedOut.split('\n'),
|
|
183
|
+
].map(s => s.trim()).filter(Boolean);
|
|
184
|
+
return [...new Set(files)];
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
export async function loadCurrentTransaction(cwd) {
|
|
191
|
+
const p = path.join(cwd, '.rigour', 'transaction-current.json');
|
|
192
|
+
if (!(await fs.pathExists(p)))
|
|
193
|
+
return null;
|
|
194
|
+
try {
|
|
195
|
+
return await fs.readJson(p);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
export async function listTransactions(cwd) {
|
|
202
|
+
const dir = path.join(cwd, '.rigour', 'transactions');
|
|
203
|
+
if (!(await fs.pathExists(dir)))
|
|
204
|
+
return [];
|
|
205
|
+
const files = (await fs.readdir(dir)).filter(f => f.endsWith('.json'));
|
|
206
|
+
const out = [];
|
|
207
|
+
for (const f of files) {
|
|
208
|
+
try {
|
|
209
|
+
out.push(await fs.readJson(path.join(dir, f)));
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// skip
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return out.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
216
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed / argv-allowlisted command runner.
|
|
3
|
+
* Replaces unrestricted shell:true for agent-facing execution.
|
|
4
|
+
*/
|
|
5
|
+
import type { PolicyEvaluation, TypedCommand } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Conservative allowlist — no general interpreters (node/python/npx) that accept -e/-c.
|
|
8
|
+
*/
|
|
9
|
+
export declare const DEFAULT_ALLOWED_BINS: Set<string>;
|
|
10
|
+
export declare function parseCommandLine(command: string): TypedCommand | null;
|
|
11
|
+
export declare function evaluateTypedCommand(command: string, allowedBins?: Set<string>): PolicyEvaluation;
|
|
12
|
+
export declare function runTypedCommand(command: string, cwd: string, allowedBins?: Set<string>): Promise<{
|
|
13
|
+
stdout: string;
|
|
14
|
+
stderr: string;
|
|
15
|
+
evaluation: PolicyEvaluation;
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed / argv-allowlisted command runner.
|
|
3
|
+
* Replaces unrestricted shell:true for agent-facing execution.
|
|
4
|
+
*/
|
|
5
|
+
import { execa } from 'execa';
|
|
6
|
+
import { hashPolicy } from './policy-hash.js';
|
|
7
|
+
/**
|
|
8
|
+
* Conservative allowlist — no general interpreters (node/python/npx) that accept -e/-c.
|
|
9
|
+
*/
|
|
10
|
+
export const DEFAULT_ALLOWED_BINS = new Set([
|
|
11
|
+
'npm',
|
|
12
|
+
'pnpm',
|
|
13
|
+
'yarn',
|
|
14
|
+
'tsc',
|
|
15
|
+
'vitest',
|
|
16
|
+
'jest',
|
|
17
|
+
'pytest',
|
|
18
|
+
'go',
|
|
19
|
+
'cargo',
|
|
20
|
+
'git',
|
|
21
|
+
'rigour',
|
|
22
|
+
'echo',
|
|
23
|
+
'cat',
|
|
24
|
+
'ls',
|
|
25
|
+
'pwd',
|
|
26
|
+
'true',
|
|
27
|
+
'false',
|
|
28
|
+
]);
|
|
29
|
+
const DENY_ARG_PATTERNS = [
|
|
30
|
+
/rm\s+-rf/i,
|
|
31
|
+
/--force/i,
|
|
32
|
+
/push\s+--force/i,
|
|
33
|
+
/force-with-lease/i,
|
|
34
|
+
/curl.*(-d|--data)/i,
|
|
35
|
+
/wget\s/i,
|
|
36
|
+
/(^|\s)-e(\s|=|$)/,
|
|
37
|
+
/(^|\s)--eval(\s|=|$)/,
|
|
38
|
+
/\bnpm\s+exec\b/i,
|
|
39
|
+
/\bnpx\b/i,
|
|
40
|
+
/\byarn\s+dlx\b/i,
|
|
41
|
+
/\bpnpm\s+dlx\b/i,
|
|
42
|
+
];
|
|
43
|
+
const GIT_DENY_SUBCOMMANDS = new Set(['push', 'reset', 'clean', 'rebase', 'filter-branch']);
|
|
44
|
+
export function parseCommandLine(command) {
|
|
45
|
+
const trimmed = command.trim();
|
|
46
|
+
if (!trimmed)
|
|
47
|
+
return null;
|
|
48
|
+
if (/[;|&`$<>]/.test(trimmed) || trimmed.includes('\n')) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const parts = trimmed.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g);
|
|
52
|
+
if (!parts || parts.length === 0)
|
|
53
|
+
return null;
|
|
54
|
+
const unquote = (s) => {
|
|
55
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
56
|
+
return s.slice(1, -1);
|
|
57
|
+
}
|
|
58
|
+
return s;
|
|
59
|
+
};
|
|
60
|
+
const [bin, ...args] = parts.map(unquote);
|
|
61
|
+
return { bin, args };
|
|
62
|
+
}
|
|
63
|
+
export function evaluateTypedCommand(command, allowedBins = DEFAULT_ALLOWED_BINS) {
|
|
64
|
+
const policyHash = hashPolicy({ allowedBins: [...allowedBins].sort() });
|
|
65
|
+
const parsed = parseCommandLine(command);
|
|
66
|
+
if (!parsed) {
|
|
67
|
+
return {
|
|
68
|
+
decision: 'deny',
|
|
69
|
+
reason: 'Command contains shell metacharacters or could not be parsed as argv',
|
|
70
|
+
ruleId: 'shell.no-meta',
|
|
71
|
+
policyHash,
|
|
72
|
+
timestamp: new Date().toISOString(),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const binBase = parsed.bin.split(/[/\\]/).pop() || parsed.bin;
|
|
76
|
+
if (!allowedBins.has(binBase) && !allowedBins.has(parsed.bin)) {
|
|
77
|
+
return {
|
|
78
|
+
decision: 'deny',
|
|
79
|
+
reason: `Binary "${binBase}" is not on the allowlist`,
|
|
80
|
+
ruleId: 'shell.allowlist',
|
|
81
|
+
policyHash,
|
|
82
|
+
timestamp: new Date().toISOString(),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const full = `${parsed.bin} ${parsed.args.join(' ')}`;
|
|
86
|
+
for (const re of DENY_ARG_PATTERNS) {
|
|
87
|
+
if (re.test(full)) {
|
|
88
|
+
return {
|
|
89
|
+
decision: 'deny',
|
|
90
|
+
reason: `Command matched deny pattern: ${re}`,
|
|
91
|
+
ruleId: 'shell.deny-pattern',
|
|
92
|
+
policyHash,
|
|
93
|
+
timestamp: new Date().toISOString(),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (binBase === 'git') {
|
|
98
|
+
const sub = parsed.args.find(a => !a.startsWith('-'));
|
|
99
|
+
if (sub && GIT_DENY_SUBCOMMANDS.has(sub)) {
|
|
100
|
+
return {
|
|
101
|
+
decision: 'deny',
|
|
102
|
+
reason: `git ${sub} requires explicit capability (destructive git mutation)`,
|
|
103
|
+
ruleId: 'git.destructive',
|
|
104
|
+
policyHash,
|
|
105
|
+
timestamp: new Date().toISOString(),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
decision: 'allow',
|
|
111
|
+
reason: `Allowlisted binary ${binBase}`,
|
|
112
|
+
ruleId: 'shell.allow',
|
|
113
|
+
policyHash,
|
|
114
|
+
timestamp: new Date().toISOString(),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export async function runTypedCommand(command, cwd, allowedBins) {
|
|
118
|
+
const evaluation = evaluateTypedCommand(command, allowedBins);
|
|
119
|
+
if (evaluation.decision !== 'allow') {
|
|
120
|
+
throw Object.assign(new Error(evaluation.reason), { evaluation, code: 'FIREWALL_DENY' });
|
|
121
|
+
}
|
|
122
|
+
const parsed = parseCommandLine(command);
|
|
123
|
+
const { stdout, stderr } = await execa(parsed.bin, parsed.args, {
|
|
124
|
+
cwd,
|
|
125
|
+
shell: false,
|
|
126
|
+
reject: false,
|
|
127
|
+
});
|
|
128
|
+
return { stdout: stdout ?? '', stderr: stderr ?? '', evaluation };
|
|
129
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Transaction Firewall — shared types.
|
|
3
|
+
* Deterministic allow/deny decisions; no LLM on the hot path.
|
|
4
|
+
*/
|
|
5
|
+
export type FirewallDecision = 'allow' | 'deny' | 'timeout-deny' | 'scope-violation';
|
|
6
|
+
export type CapabilityAction = 'filesystem.write' | 'filesystem.read' | 'shell.exec' | 'mcp.call' | 'git.mutate' | 'secret.lease' | 'network.egress' | 'deploy';
|
|
7
|
+
export interface CapabilityGrant {
|
|
8
|
+
id: string;
|
|
9
|
+
action: CapabilityAction;
|
|
10
|
+
resource: string;
|
|
11
|
+
constraints?: Record<string, unknown>;
|
|
12
|
+
expiresAt: number;
|
|
13
|
+
agentId?: string;
|
|
14
|
+
taskId?: string;
|
|
15
|
+
policyHash: string;
|
|
16
|
+
used: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface PolicyEvaluation {
|
|
19
|
+
decision: FirewallDecision;
|
|
20
|
+
reason: string;
|
|
21
|
+
ruleId?: string;
|
|
22
|
+
capabilityId?: string;
|
|
23
|
+
policyHash: string;
|
|
24
|
+
timestamp: string;
|
|
25
|
+
}
|
|
26
|
+
export interface TypedCommand {
|
|
27
|
+
bin: string;
|
|
28
|
+
args: string[];
|
|
29
|
+
cwd?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface TransactionBudgets {
|
|
32
|
+
maxFiles: number;
|
|
33
|
+
maxRetries: number;
|
|
34
|
+
maxDurationMs: number;
|
|
35
|
+
maxCostUsd?: number;
|
|
36
|
+
}
|
|
37
|
+
export type TransactionStatus = 'START' | 'RUNNING' | 'VERIFYING' | 'COMMIT' | 'DISCARD';
|
|
38
|
+
export interface TransactionRecord {
|
|
39
|
+
id: string;
|
|
40
|
+
agentId?: string;
|
|
41
|
+
scope: string[];
|
|
42
|
+
status: TransactionStatus;
|
|
43
|
+
worktreePath?: string;
|
|
44
|
+
budgets: TransactionBudgets;
|
|
45
|
+
filesChanged: string[];
|
|
46
|
+
capabilitiesIssued: string[];
|
|
47
|
+
decisions: PolicyEvaluation[];
|
|
48
|
+
startedAt: string;
|
|
49
|
+
finishedAt?: string;
|
|
50
|
+
policyHash: string;
|
|
51
|
+
}
|
|
52
|
+
export interface AttestationBundle {
|
|
53
|
+
version: 1;
|
|
54
|
+
transactionId: string;
|
|
55
|
+
agentId?: string;
|
|
56
|
+
userId?: string;
|
|
57
|
+
scope: string[];
|
|
58
|
+
policyHash: string;
|
|
59
|
+
capabilities: string[];
|
|
60
|
+
filesUsed: string[];
|
|
61
|
+
toolsUsed: string[];
|
|
62
|
+
gateResults: {
|
|
63
|
+
status: string;
|
|
64
|
+
score?: number;
|
|
65
|
+
failedGates: string[];
|
|
66
|
+
};
|
|
67
|
+
overrides: string[];
|
|
68
|
+
artifactDigest: string;
|
|
69
|
+
/** git commit SHA at attestation time */
|
|
70
|
+
commitSha?: string;
|
|
71
|
+
/** git tree object id (HEAD^{tree}) */
|
|
72
|
+
treeDigest?: string;
|
|
73
|
+
signedAt: string;
|
|
74
|
+
signature: string;
|
|
75
|
+
}
|
|
76
|
+
export interface AdversarialCase {
|
|
77
|
+
id: string;
|
|
78
|
+
description: string;
|
|
79
|
+
proposedAction: {
|
|
80
|
+
action: CapabilityAction;
|
|
81
|
+
resource: string;
|
|
82
|
+
args?: Record<string, unknown>;
|
|
83
|
+
command?: string;
|
|
84
|
+
};
|
|
85
|
+
expectedDecision: FirewallDecision;
|
|
86
|
+
agentScope?: string[];
|
|
87
|
+
allowlist?: string[];
|
|
88
|
+
}
|
|
89
|
+
export interface AdversarialCaseResult {
|
|
90
|
+
caseId: string;
|
|
91
|
+
expected: FirewallDecision;
|
|
92
|
+
actual: FirewallDecision;
|
|
93
|
+
passed: boolean;
|
|
94
|
+
reason: string;
|
|
95
|
+
suggestedRule?: string;
|
|
96
|
+
}
|
package/dist/hooks/checker.d.ts
CHANGED
package/dist/hooks/checker.js
CHANGED
|
@@ -12,6 +12,7 @@ import path from 'path';
|
|
|
12
12
|
import yaml from 'yaml';
|
|
13
13
|
import { ConfigSchema } from '../types/index.js';
|
|
14
14
|
import { scanInputForCredentials } from './input-validator.js';
|
|
15
|
+
import { evaluateWriteScope, loadAgentScopesFromDisk } from '../firewall/scope-enforcement.js';
|
|
15
16
|
const JS_TS_PATTERN = /\.(ts|tsx|js|jsx|mts|mjs)$/;
|
|
16
17
|
/**
|
|
17
18
|
* Check if a file matches any ignore pattern from rigour.yml.
|
|
@@ -92,14 +93,17 @@ function checkFile(content, relPath, cwd, config) {
|
|
|
92
93
|
*/
|
|
93
94
|
export async function runHookChecker(options) {
|
|
94
95
|
const start = Date.now();
|
|
95
|
-
const { cwd, files, timeout_ms = 5000 } = options;
|
|
96
|
+
const { cwd, files, timeout_ms = 5000, agentId } = options;
|
|
96
97
|
const failures = [];
|
|
98
|
+
let timedOut = false;
|
|
97
99
|
try {
|
|
98
100
|
const config = await loadConfig(cwd);
|
|
99
101
|
const deadline = start + timeout_ms;
|
|
100
102
|
const ignorePatterns = config.ignore ?? [];
|
|
103
|
+
const agentScopes = await loadAgentScopesFromDisk(cwd);
|
|
101
104
|
for (const filePath of files) {
|
|
102
105
|
if (Date.now() > deadline) {
|
|
106
|
+
timedOut = true;
|
|
103
107
|
break;
|
|
104
108
|
}
|
|
105
109
|
const resolved = await resolveFile(filePath, cwd);
|
|
@@ -110,9 +114,29 @@ export async function runHookChecker(options) {
|
|
|
110
114
|
if (isIgnored(resolved.relPath, ignorePatterns)) {
|
|
111
115
|
continue;
|
|
112
116
|
}
|
|
117
|
+
if (agentScopes.length > 0) {
|
|
118
|
+
const scopeEv = evaluateWriteScope(cwd, resolved.relPath, agentScopes, agentId);
|
|
119
|
+
if (scopeEv.decision !== 'allow') {
|
|
120
|
+
failures.push({
|
|
121
|
+
gate: 'agent-scope',
|
|
122
|
+
file: resolved.relPath,
|
|
123
|
+
message: scopeEv.reason,
|
|
124
|
+
severity: 'critical',
|
|
125
|
+
});
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
113
129
|
const fileFailures = checkFile(resolved.content, resolved.relPath, cwd, config);
|
|
114
130
|
failures.push(...fileFailures);
|
|
115
131
|
}
|
|
132
|
+
if (timedOut) {
|
|
133
|
+
failures.push({
|
|
134
|
+
gate: 'hook-timeout',
|
|
135
|
+
file: '',
|
|
136
|
+
message: `Hook checker exceeded ${timeout_ms}ms before all files were scanned (fail-closed)`,
|
|
137
|
+
severity: 'critical',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
116
140
|
return {
|
|
117
141
|
status: failures.length > 0 ? 'fail' : 'pass',
|
|
118
142
|
failures,
|
|
@@ -89,7 +89,11 @@ async function main() {
|
|
|
89
89
|
process.stdout.write(EMPTY_RESULT);
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
|
-
const result = await runHookChecker({
|
|
92
|
+
const result = await runHookChecker({
|
|
93
|
+
cwd,
|
|
94
|
+
files,
|
|
95
|
+
agentId: process.env.RIGOUR_AGENT_ID || undefined,
|
|
96
|
+
});
|
|
93
97
|
process.stdout.write(JSON.stringify(result));
|
|
94
98
|
if (result.status === 'fail') {
|
|
95
99
|
logFailures(result.failures);
|
package/dist/hooks/templates.js
CHANGED
|
@@ -82,6 +82,7 @@ process.stdin.on('end', async () => {
|
|
|
82
82
|
const result = await runHookChecker({
|
|
83
83
|
cwd: process.cwd(),
|
|
84
84
|
files: [payload.file_path],
|
|
85
|
+
agentId: process.env.RIGOUR_AGENT_ID || undefined,
|
|
85
86
|
});
|
|
86
87
|
|
|
87
88
|
// Write result to stdout for Cursor to consume
|
|
@@ -148,6 +149,7 @@ process.stdin.on('end', async () => {
|
|
|
148
149
|
const result = await runHookChecker({
|
|
149
150
|
cwd: process.cwd(),
|
|
150
151
|
files: [filePath],
|
|
152
|
+
agentId: process.env.RIGOUR_AGENT_ID || undefined,
|
|
151
153
|
});
|
|
152
154
|
|
|
153
155
|
if (result.status === 'fail') {
|
|
@@ -206,6 +208,7 @@ process.stdin.on('end', async () => {
|
|
|
206
208
|
const result = await runHookChecker({
|
|
207
209
|
cwd: process.cwd(),
|
|
208
210
|
files: [payload.file_path],
|
|
211
|
+
agentId: process.env.RIGOUR_AGENT_ID || undefined,
|
|
209
212
|
});
|
|
210
213
|
|
|
211
214
|
if (result.status === 'fail') {
|
package/dist/index.d.ts
CHANGED
|
@@ -36,4 +36,5 @@ export * from './context/cache-engine.js';
|
|
|
36
36
|
export * from './context/context-session.js';
|
|
37
37
|
export * from './context/index-bridge.js';
|
|
38
38
|
export * from './services/context-telemetry-service.js';
|
|
39
|
+
export * from './firewall/index.js';
|
|
39
40
|
export { recordContextEvent, recordModelUsage, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics, } from './storage/index.js';
|
package/dist/index.js
CHANGED
|
@@ -37,6 +37,7 @@ export * from './context/cache-engine.js';
|
|
|
37
37
|
export * from './context/context-session.js';
|
|
38
38
|
export * from './context/index-bridge.js';
|
|
39
39
|
export * from './services/context-telemetry-service.js';
|
|
40
|
+
export * from './firewall/index.js';
|
|
40
41
|
export { recordContextEvent, recordModelUsage, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics, } from './storage/index.js';
|
|
41
42
|
// Pattern Index is intentionally NOT exported here to prevent
|
|
42
43
|
// native dependency issues (sharp/transformers) from leaking into
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "AI-native quality gate engine with local Bayesian learning. AST analysis, drift detection, Fix Packet generation, and agent self-healing across TypeScript, JavaScript, Python, Go, Ruby, and C#.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://rigour.run",
|
|
@@ -66,11 +66,11 @@
|
|
|
66
66
|
"@xenova/transformers": "^2.17.2",
|
|
67
67
|
"sqlite3": "^5.1.7",
|
|
68
68
|
"openai": "^4.104.0",
|
|
69
|
-
"@rigour-labs/brain-darwin-arm64": "
|
|
70
|
-
"@rigour-labs/brain-
|
|
71
|
-
"@rigour-labs/brain-
|
|
72
|
-
"@rigour-labs/brain-
|
|
73
|
-
"@rigour-labs/brain-
|
|
69
|
+
"@rigour-labs/brain-darwin-arm64": "6.0.0",
|
|
70
|
+
"@rigour-labs/brain-linux-arm64": "6.0.0",
|
|
71
|
+
"@rigour-labs/brain-darwin-x64": "6.0.0",
|
|
72
|
+
"@rigour-labs/brain-win-x64": "6.0.0",
|
|
73
|
+
"@rigour-labs/brain-linux-x64": "6.0.0"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"@types/fs-extra": "^11.0.4",
|