@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,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adversarial replay — deterministic corpus against the firewall kernel.
|
|
3
|
+
* Successful unexpected allows emit suggested deny regressions (not LLM verdicts).
|
|
4
|
+
*/
|
|
5
|
+
import type { AdversarialCase, AdversarialCaseResult } from './types.js';
|
|
6
|
+
export declare const DEFAULT_ADVERSARIAL_CORPUS: AdversarialCase[];
|
|
7
|
+
export declare function runAdversarialCase(c: AdversarialCase): AdversarialCaseResult;
|
|
8
|
+
export declare function runAdversarialCorpus(cases?: AdversarialCase[]): {
|
|
9
|
+
passed: number;
|
|
10
|
+
failed: number;
|
|
11
|
+
results: AdversarialCaseResult[];
|
|
12
|
+
};
|
|
13
|
+
export declare function persistAdversarialReport(cwd: string, report: ReturnType<typeof runAdversarialCorpus>): Promise<string>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adversarial replay — deterministic corpus against the firewall kernel.
|
|
3
|
+
* Successful unexpected allows emit suggested deny regressions (not LLM verdicts).
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { evaluateActionDeterministic } from './capability-broker.js';
|
|
8
|
+
export const DEFAULT_ADVERSARIAL_CORPUS = [
|
|
9
|
+
{
|
|
10
|
+
id: 'deny-shell-meta',
|
|
11
|
+
description: 'Shell metacharacters must be denied',
|
|
12
|
+
proposedAction: {
|
|
13
|
+
action: 'shell.exec',
|
|
14
|
+
resource: 'shell',
|
|
15
|
+
command: 'ls; rm -rf /',
|
|
16
|
+
},
|
|
17
|
+
expectedDecision: 'deny',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'deny-undeclared-mcp',
|
|
21
|
+
description: 'Undeclared MCP tool must be denied',
|
|
22
|
+
proposedAction: {
|
|
23
|
+
action: 'mcp.call',
|
|
24
|
+
resource: 'github.merge_pull_request',
|
|
25
|
+
},
|
|
26
|
+
expectedDecision: 'deny',
|
|
27
|
+
allowlist: ['rigour_check'],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'deny-out-of-scope-write',
|
|
31
|
+
description: 'Write outside agent scope must be scope-violation',
|
|
32
|
+
proposedAction: {
|
|
33
|
+
action: 'filesystem.write',
|
|
34
|
+
resource: '.github/workflows/deploy.yml',
|
|
35
|
+
},
|
|
36
|
+
expectedDecision: 'scope-violation',
|
|
37
|
+
agentScope: ['packages/rigour-core/**'],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'deny-git-force-push',
|
|
41
|
+
description: 'git push --force must be denied',
|
|
42
|
+
proposedAction: {
|
|
43
|
+
action: 'shell.exec',
|
|
44
|
+
resource: 'shell',
|
|
45
|
+
command: 'git push --force origin main',
|
|
46
|
+
},
|
|
47
|
+
expectedDecision: 'deny',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'allow-scoped-write',
|
|
51
|
+
description: 'In-scope write is allowed',
|
|
52
|
+
proposedAction: {
|
|
53
|
+
action: 'filesystem.write',
|
|
54
|
+
resource: 'packages/rigour-core/src/firewall/types.ts',
|
|
55
|
+
},
|
|
56
|
+
expectedDecision: 'allow',
|
|
57
|
+
agentScope: ['packages/rigour-core/**'],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'allow-typed-test',
|
|
61
|
+
description: 'Allowlisted vitest command is allowed',
|
|
62
|
+
proposedAction: {
|
|
63
|
+
action: 'shell.exec',
|
|
64
|
+
resource: 'shell',
|
|
65
|
+
command: 'vitest run packages/rigour-core/src/firewall',
|
|
66
|
+
},
|
|
67
|
+
expectedDecision: 'allow',
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
function normalizeDecision(d) {
|
|
71
|
+
return d;
|
|
72
|
+
}
|
|
73
|
+
export function runAdversarialCase(c) {
|
|
74
|
+
const agentScopes = c.agentScope
|
|
75
|
+
? [{ agentId: 'adversarial-agent', taskScope: c.agentScope }]
|
|
76
|
+
: [];
|
|
77
|
+
const evaluation = evaluateActionDeterministic({
|
|
78
|
+
action: c.proposedAction.action,
|
|
79
|
+
resource: c.proposedAction.resource,
|
|
80
|
+
command: c.proposedAction.command,
|
|
81
|
+
toolAllowlist: c.allowlist ?? [],
|
|
82
|
+
agentScopes,
|
|
83
|
+
agentId: c.agentScope ? 'adversarial-agent' : undefined,
|
|
84
|
+
});
|
|
85
|
+
const actual = normalizeDecision(evaluation.decision);
|
|
86
|
+
const passed = actual === c.expectedDecision;
|
|
87
|
+
let suggestedRule;
|
|
88
|
+
if (!passed && (actual === 'allow')) {
|
|
89
|
+
suggestedRule = [
|
|
90
|
+
'deny:',
|
|
91
|
+
` - tool: ${c.proposedAction.action}`,
|
|
92
|
+
` resource: ${JSON.stringify(c.proposedAction.resource)}`,
|
|
93
|
+
c.proposedAction.command ? ` command_matches: ${JSON.stringify(c.proposedAction.command)}` : null,
|
|
94
|
+
].filter(Boolean).join('\n');
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
caseId: c.id,
|
|
98
|
+
expected: c.expectedDecision,
|
|
99
|
+
actual,
|
|
100
|
+
passed,
|
|
101
|
+
reason: evaluation.reason,
|
|
102
|
+
suggestedRule,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export function runAdversarialCorpus(cases = DEFAULT_ADVERSARIAL_CORPUS) {
|
|
106
|
+
const results = cases.map(runAdversarialCase);
|
|
107
|
+
return {
|
|
108
|
+
passed: results.filter(r => r.passed).length,
|
|
109
|
+
failed: results.filter(r => !r.passed).length,
|
|
110
|
+
results,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export async function persistAdversarialReport(cwd, report) {
|
|
114
|
+
const dir = path.join(cwd, '.rigour');
|
|
115
|
+
await fs.ensureDir(dir);
|
|
116
|
+
const out = path.join(dir, 'adversarial-report.json');
|
|
117
|
+
await fs.writeJson(out, { ...report, generatedAt: new Date().toISOString() }, { spaces: 2 });
|
|
118
|
+
const regressions = report.results.filter(r => !r.passed && r.suggestedRule);
|
|
119
|
+
if (regressions.length > 0) {
|
|
120
|
+
const fixturePath = path.join(dir, 'adversarial-regressions.yml');
|
|
121
|
+
const body = regressions.map(r => `# from ${r.caseId}\n${r.suggestedRule}`).join('\n\n');
|
|
122
|
+
await fs.writeFile(fixturePath, body + '\n');
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time arbitration tokens — bind Studio approve/reject to the requesting MCP process.
|
|
3
|
+
*/
|
|
4
|
+
export declare function issueArbitrationToken(cwd: string, requestId: string, ttlMs?: number): Promise<string>;
|
|
5
|
+
/**
|
|
6
|
+
* Consume a one-time token. Returns false if missing, expired, or mismatched.
|
|
7
|
+
*/
|
|
8
|
+
export declare function consumeArbitrationToken(cwd: string, requestId: string, token: string | undefined): Promise<boolean>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time arbitration tokens — bind Studio approve/reject to the requesting MCP process.
|
|
3
|
+
*/
|
|
4
|
+
import { randomBytes, timingSafeEqual } from 'crypto';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
function storePath(cwd) {
|
|
8
|
+
return path.join(cwd, '.rigour', 'arbitration-tokens.json');
|
|
9
|
+
}
|
|
10
|
+
async function readStore(cwd) {
|
|
11
|
+
const p = storePath(cwd);
|
|
12
|
+
if (!(await fs.pathExists(p)))
|
|
13
|
+
return {};
|
|
14
|
+
try {
|
|
15
|
+
return await fs.readJson(p);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function writeStore(cwd, store) {
|
|
22
|
+
await fs.ensureDir(path.join(cwd, '.rigour'));
|
|
23
|
+
await fs.writeJson(storePath(cwd), store, { spaces: 2 });
|
|
24
|
+
}
|
|
25
|
+
export async function issueArbitrationToken(cwd, requestId, ttlMs = 60_000) {
|
|
26
|
+
const token = randomBytes(24).toString('hex');
|
|
27
|
+
const store = await readStore(cwd);
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
for (const [k, v] of Object.entries(store)) {
|
|
30
|
+
if (v.expiresAt < now)
|
|
31
|
+
delete store[k];
|
|
32
|
+
}
|
|
33
|
+
store[requestId] = { token, expiresAt: now + ttlMs };
|
|
34
|
+
await writeStore(cwd, store);
|
|
35
|
+
return token;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Consume a one-time token. Returns false if missing, expired, or mismatched.
|
|
39
|
+
*/
|
|
40
|
+
export async function consumeArbitrationToken(cwd, requestId, token) {
|
|
41
|
+
if (!token || !requestId)
|
|
42
|
+
return false;
|
|
43
|
+
const store = await readStore(cwd);
|
|
44
|
+
const entry = store[requestId];
|
|
45
|
+
if (!entry)
|
|
46
|
+
return false;
|
|
47
|
+
if (Date.now() > entry.expiresAt) {
|
|
48
|
+
delete store[requestId];
|
|
49
|
+
await writeStore(cwd, store);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const a = Buffer.from(entry.token);
|
|
53
|
+
const b = Buffer.from(token);
|
|
54
|
+
const ok = a.length === b.length && timingSafeEqual(a, b);
|
|
55
|
+
delete store[requestId];
|
|
56
|
+
await writeStore(cwd, store);
|
|
57
|
+
return ok;
|
|
58
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signed attestation bundles for completed transactions.
|
|
3
|
+
* Keys prefer env / home directory — not agent-writable workspace keys for CI admit.
|
|
4
|
+
*/
|
|
5
|
+
import type { AttestationBundle, TransactionRecord } from './types.js';
|
|
6
|
+
export declare function resolveAttestationKey(cwd: string): Promise<{
|
|
7
|
+
key: Buffer;
|
|
8
|
+
source: 'env' | 'home' | 'workspace';
|
|
9
|
+
}>;
|
|
10
|
+
/** @deprecated use resolveAttestationKey */
|
|
11
|
+
export declare function ensureAttestationKey(cwd: string): Promise<Buffer>;
|
|
12
|
+
export declare function computeArtifactDigest(files: string[], contents: Map<string, string>): string;
|
|
13
|
+
export declare function getGitCommitSha(dir: string): Promise<string | null>;
|
|
14
|
+
export declare function getGitTreeDigest(dir: string): Promise<string | null>;
|
|
15
|
+
export declare function createAttestation(cwd: string, input: {
|
|
16
|
+
transaction: TransactionRecord;
|
|
17
|
+
gateResults: {
|
|
18
|
+
status: string;
|
|
19
|
+
score?: number;
|
|
20
|
+
failedGates: string[];
|
|
21
|
+
};
|
|
22
|
+
toolsUsed?: string[];
|
|
23
|
+
overrides?: string[];
|
|
24
|
+
userId?: string;
|
|
25
|
+
fileContents?: Map<string, string>;
|
|
26
|
+
commitSha?: string;
|
|
27
|
+
treeDigest?: string;
|
|
28
|
+
verifyRoot?: string;
|
|
29
|
+
}): Promise<AttestationBundle>;
|
|
30
|
+
export declare function verifyAttestation(cwd: string, bundle: AttestationBundle): Promise<boolean>;
|
|
31
|
+
export declare function loadLatestAttestation(cwd: string): Promise<AttestationBundle | null>;
|
|
32
|
+
/**
|
|
33
|
+
* CI admission: valid signature, PASS gates, bound commit/tree, fresh, non-workspace-key unless allowed.
|
|
34
|
+
*/
|
|
35
|
+
export declare function admitForCi(cwd: string): Promise<{
|
|
36
|
+
admit: boolean;
|
|
37
|
+
reason: string;
|
|
38
|
+
}>;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signed attestation bundles for completed transactions.
|
|
3
|
+
* Keys prefer env / home directory — not agent-writable workspace keys for CI admit.
|
|
4
|
+
*/
|
|
5
|
+
import { createHash, createHmac, randomBytes } from 'crypto';
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { execa } from 'execa';
|
|
10
|
+
const KEY_FILE = 'attestation.key';
|
|
11
|
+
export async function resolveAttestationKey(cwd) {
|
|
12
|
+
const envKey = process.env.RIGOUR_ATTESTATION_KEY;
|
|
13
|
+
if (envKey && envKey.length >= 32) {
|
|
14
|
+
return { key: Buffer.from(envKey, 'utf8'), source: 'env' };
|
|
15
|
+
}
|
|
16
|
+
const homeKeyPath = path.join(os.homedir(), '.rigour', KEY_FILE);
|
|
17
|
+
if (await fs.pathExists(homeKeyPath)) {
|
|
18
|
+
return { key: await fs.readFile(homeKeyPath), source: 'home' };
|
|
19
|
+
}
|
|
20
|
+
const workspaceKeyPath = path.join(cwd, '.rigour', KEY_FILE);
|
|
21
|
+
if (await fs.pathExists(workspaceKeyPath)) {
|
|
22
|
+
return { key: await fs.readFile(workspaceKeyPath), source: 'workspace' };
|
|
23
|
+
}
|
|
24
|
+
// Create home key by default (outside agent workspace)
|
|
25
|
+
await fs.ensureDir(path.join(os.homedir(), '.rigour'));
|
|
26
|
+
const key = randomBytes(32);
|
|
27
|
+
await fs.writeFile(homeKeyPath, key, { mode: 0o600 });
|
|
28
|
+
return { key, source: 'home' };
|
|
29
|
+
}
|
|
30
|
+
/** @deprecated use resolveAttestationKey */
|
|
31
|
+
export async function ensureAttestationKey(cwd) {
|
|
32
|
+
const { key } = await resolveAttestationKey(cwd);
|
|
33
|
+
return key;
|
|
34
|
+
}
|
|
35
|
+
function payloadForSign(bundle) {
|
|
36
|
+
return JSON.stringify({
|
|
37
|
+
version: bundle.version,
|
|
38
|
+
transactionId: bundle.transactionId,
|
|
39
|
+
agentId: bundle.agentId,
|
|
40
|
+
userId: bundle.userId,
|
|
41
|
+
scope: bundle.scope,
|
|
42
|
+
policyHash: bundle.policyHash,
|
|
43
|
+
capabilities: bundle.capabilities,
|
|
44
|
+
filesUsed: bundle.filesUsed,
|
|
45
|
+
toolsUsed: bundle.toolsUsed,
|
|
46
|
+
gateResults: bundle.gateResults,
|
|
47
|
+
overrides: bundle.overrides,
|
|
48
|
+
artifactDigest: bundle.artifactDigest,
|
|
49
|
+
commitSha: bundle.commitSha,
|
|
50
|
+
treeDigest: bundle.treeDigest,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export function computeArtifactDigest(files, contents) {
|
|
54
|
+
const h = createHash('sha256');
|
|
55
|
+
for (const f of [...files].sort()) {
|
|
56
|
+
h.update(f);
|
|
57
|
+
h.update('\0');
|
|
58
|
+
h.update(contents.get(f) ?? '');
|
|
59
|
+
h.update('\0');
|
|
60
|
+
}
|
|
61
|
+
return h.digest('hex');
|
|
62
|
+
}
|
|
63
|
+
export async function getGitCommitSha(dir) {
|
|
64
|
+
try {
|
|
65
|
+
const { stdout } = await execa('git', ['rev-parse', 'HEAD'], { cwd: dir, shell: false });
|
|
66
|
+
return stdout.trim() || null;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export async function getGitTreeDigest(dir) {
|
|
73
|
+
try {
|
|
74
|
+
const { stdout } = await execa('git', ['rev-parse', 'HEAD^{tree}'], { cwd: dir, shell: false });
|
|
75
|
+
return stdout.trim() || null;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export async function createAttestation(cwd, input) {
|
|
82
|
+
const { key } = await resolveAttestationKey(cwd);
|
|
83
|
+
const verifyRoot = input.verifyRoot || input.transaction.worktreePath || cwd;
|
|
84
|
+
const commitSha = input.commitSha ?? (await getGitCommitSha(verifyRoot)) ?? undefined;
|
|
85
|
+
const treeDigest = input.treeDigest ?? (await getGitTreeDigest(verifyRoot)) ?? undefined;
|
|
86
|
+
const files = input.transaction.filesChanged;
|
|
87
|
+
const contents = input.fileContents ?? new Map();
|
|
88
|
+
if (files.length > 0 && contents.size === 0) {
|
|
89
|
+
for (const f of files) {
|
|
90
|
+
const abs = path.join(verifyRoot, f);
|
|
91
|
+
if (await fs.pathExists(abs)) {
|
|
92
|
+
contents.set(f, await fs.readFile(abs, 'utf-8'));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
let artifactDigest = computeArtifactDigest(files, contents);
|
|
97
|
+
if (files.length === 0 && treeDigest) {
|
|
98
|
+
artifactDigest = createHash('sha256').update(`tree:${treeDigest}`).digest('hex');
|
|
99
|
+
}
|
|
100
|
+
if (!treeDigest && files.length === 0) {
|
|
101
|
+
throw new Error('Attestation requires treeDigest or non-empty filesChanged with contents');
|
|
102
|
+
}
|
|
103
|
+
const unsigned = {
|
|
104
|
+
version: 1,
|
|
105
|
+
transactionId: input.transaction.id,
|
|
106
|
+
agentId: input.transaction.agentId,
|
|
107
|
+
userId: input.userId,
|
|
108
|
+
scope: input.transaction.scope,
|
|
109
|
+
policyHash: input.transaction.policyHash,
|
|
110
|
+
capabilities: input.transaction.capabilitiesIssued,
|
|
111
|
+
filesUsed: files,
|
|
112
|
+
toolsUsed: input.toolsUsed ?? [],
|
|
113
|
+
gateResults: input.gateResults,
|
|
114
|
+
overrides: input.overrides ?? [],
|
|
115
|
+
artifactDigest,
|
|
116
|
+
commitSha,
|
|
117
|
+
treeDigest,
|
|
118
|
+
};
|
|
119
|
+
const signedAt = new Date().toISOString();
|
|
120
|
+
const signature = createHmac('sha256', key)
|
|
121
|
+
.update(payloadForSign(unsigned) + signedAt)
|
|
122
|
+
.digest('hex');
|
|
123
|
+
const bundle = { ...unsigned, signedAt, signature };
|
|
124
|
+
const outDir = path.join(cwd, '.rigour', 'attestations');
|
|
125
|
+
await fs.ensureDir(outDir);
|
|
126
|
+
await fs.writeJson(path.join(outDir, `${bundle.transactionId}.json`), bundle, { spaces: 2 });
|
|
127
|
+
await fs.writeJson(path.join(cwd, '.rigour', 'attestation-latest.json'), bundle, { spaces: 2 });
|
|
128
|
+
return bundle;
|
|
129
|
+
}
|
|
130
|
+
export async function verifyAttestation(cwd, bundle) {
|
|
131
|
+
const { key } = await resolveAttestationKey(cwd);
|
|
132
|
+
const { signature, signedAt, ...rest } = bundle;
|
|
133
|
+
const expected = createHmac('sha256', key)
|
|
134
|
+
.update(payloadForSign(rest) + signedAt)
|
|
135
|
+
.digest('hex');
|
|
136
|
+
return expected === signature;
|
|
137
|
+
}
|
|
138
|
+
export async function loadLatestAttestation(cwd) {
|
|
139
|
+
const p = path.join(cwd, '.rigour', 'attestation-latest.json');
|
|
140
|
+
if (!(await fs.pathExists(p)))
|
|
141
|
+
return null;
|
|
142
|
+
try {
|
|
143
|
+
return await fs.readJson(p);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* CI admission: valid signature, PASS gates, bound commit/tree, fresh, non-workspace-key unless allowed.
|
|
151
|
+
*/
|
|
152
|
+
export async function admitForCi(cwd) {
|
|
153
|
+
const { source } = await resolveAttestationKey(cwd);
|
|
154
|
+
if (source === 'workspace' && process.env.RIGOUR_ALLOW_WORKSPACE_ATTESTATION_KEY !== '1') {
|
|
155
|
+
return {
|
|
156
|
+
admit: false,
|
|
157
|
+
reason: 'Attestation key is workspace-local; set RIGOUR_ATTESTATION_KEY or ~/.rigour/attestation.key',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const bundle = await loadLatestAttestation(cwd);
|
|
161
|
+
if (!bundle) {
|
|
162
|
+
return { admit: false, reason: 'No attestation bundle found' };
|
|
163
|
+
}
|
|
164
|
+
const valid = await verifyAttestation(cwd, bundle);
|
|
165
|
+
if (!valid) {
|
|
166
|
+
return { admit: false, reason: 'Attestation signature invalid' };
|
|
167
|
+
}
|
|
168
|
+
if (bundle.gateResults.status !== 'PASS') {
|
|
169
|
+
return { admit: false, reason: `Gates not PASS (${bundle.gateResults.status})` };
|
|
170
|
+
}
|
|
171
|
+
if (!bundle.treeDigest && (!bundle.filesUsed || bundle.filesUsed.length === 0)) {
|
|
172
|
+
return { admit: false, reason: 'Attestation missing treeDigest and filesUsed' };
|
|
173
|
+
}
|
|
174
|
+
if (!bundle.artifactDigest || bundle.artifactDigest.length < 16) {
|
|
175
|
+
return { admit: false, reason: 'Attestation artifactDigest missing or trivial' };
|
|
176
|
+
}
|
|
177
|
+
const maxAgeMs = Number(process.env.RIGOUR_ATTESTATION_MAX_AGE_MS || 24 * 60 * 60 * 1000);
|
|
178
|
+
const age = Date.now() - Date.parse(bundle.signedAt);
|
|
179
|
+
if (!Number.isFinite(age) || age < 0 || age > maxAgeMs) {
|
|
180
|
+
return { admit: false, reason: 'Attestation expired or has invalid signedAt' };
|
|
181
|
+
}
|
|
182
|
+
const headSha = await getGitCommitSha(cwd);
|
|
183
|
+
if (bundle.commitSha && headSha && bundle.commitSha !== headSha) {
|
|
184
|
+
return {
|
|
185
|
+
admit: false,
|
|
186
|
+
reason: `Attestation commitSha ${bundle.commitSha.slice(0, 8)}≠ HEAD ${headSha.slice(0, 8)}`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const headTree = await getGitTreeDigest(cwd);
|
|
190
|
+
if (bundle.treeDigest && headTree && bundle.treeDigest !== headTree) {
|
|
191
|
+
return {
|
|
192
|
+
admit: false,
|
|
193
|
+
reason: `Attestation treeDigest mismatch vs HEAD tree`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return { admit: true, reason: 'Valid attestation with PASS gates and bound tree/commit' };
|
|
197
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability broker — issues one-use, short-lived capabilities.
|
|
3
|
+
*/
|
|
4
|
+
import type { CapabilityAction, CapabilityGrant, PolicyEvaluation } from './types.js';
|
|
5
|
+
import { type AgentScopeRecord } from './scope-enforcement.js';
|
|
6
|
+
export interface BrokerConfig {
|
|
7
|
+
defaultTtlMs: number;
|
|
8
|
+
toolAllowlist: string[];
|
|
9
|
+
agentScopes: AgentScopeRecord[];
|
|
10
|
+
agentId?: string;
|
|
11
|
+
taskId?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare class CapabilityBroker {
|
|
14
|
+
private readonly config;
|
|
15
|
+
private grants;
|
|
16
|
+
private decisions;
|
|
17
|
+
private readonly policyHash;
|
|
18
|
+
constructor(config: BrokerConfig);
|
|
19
|
+
getPolicyHash(): string;
|
|
20
|
+
getDecisions(): PolicyEvaluation[];
|
|
21
|
+
listGrants(): CapabilityGrant[];
|
|
22
|
+
issue(action: CapabilityAction, resource: string, constraints?: Record<string, unknown>): CapabilityGrant;
|
|
23
|
+
evaluateProposal(input: {
|
|
24
|
+
action: CapabilityAction;
|
|
25
|
+
resource: string;
|
|
26
|
+
args?: Record<string, unknown>;
|
|
27
|
+
command?: string;
|
|
28
|
+
capabilityId?: string;
|
|
29
|
+
}): PolicyEvaluation;
|
|
30
|
+
private prechecks;
|
|
31
|
+
private denyUndeclaredMcp;
|
|
32
|
+
private denyOutOfScopeWrite;
|
|
33
|
+
private evaluateWithCapability;
|
|
34
|
+
private consumeCapability;
|
|
35
|
+
private denyCap;
|
|
36
|
+
private record;
|
|
37
|
+
persistDecisions(cwd: string): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
/** Pure evaluate for adversarial replay (fresh broker per call). */
|
|
40
|
+
export declare function evaluateActionDeterministic(input: {
|
|
41
|
+
action: CapabilityAction;
|
|
42
|
+
resource: string;
|
|
43
|
+
command?: string;
|
|
44
|
+
toolAllowlist: string[];
|
|
45
|
+
agentScopes: AgentScopeRecord[];
|
|
46
|
+
agentId?: string;
|
|
47
|
+
}): PolicyEvaluation;
|