@agilno-tech/rivet 0.1.0-alpha.0 → 0.1.0-alpha.2

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.
@@ -0,0 +1,225 @@
1
+ const IDENTITY = ['phase', 'runId', 'requestDigest', 'planDigest', 'baseSha', 'headSha', 'diffDigest'];
2
+ const DIGEST = /^(?:sha256:)?[a-f0-9]{64}$/;
3
+ const SHA = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
4
+ const ID = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/;
5
+
6
+ export class ContentReviewError extends Error {
7
+ constructor(reason) {
8
+ super(`Content review is invalid: ${reason}.`);
9
+ this.name = 'ContentReviewError';
10
+ this.code = 'ERR_CONTENT_REVIEW';
11
+ this.safeMessage = this.message;
12
+ this.details = Object.freeze({ reason });
13
+ }
14
+ }
15
+ const fail = reason => { throw new ContentReviewError(reason); };
16
+ function object(value, keys, required = keys) {
17
+ if (!value || typeof value !== 'object' || Array.isArray(value)) fail('invalid-object');
18
+ const descriptors = Object.getOwnPropertyDescriptors(value);
19
+ if (Reflect.ownKeys(descriptors).some(key => typeof key !== 'string' || !keys.includes(key)
20
+ || !descriptors[key].enumerable || !Object.hasOwn(descriptors[key], 'value'))
21
+ || required.some(key => !Object.hasOwn(descriptors, key))) fail('invalid-fields');
22
+ return Object.fromEntries(Object.entries(descriptors).map(([key, d]) => [key, d.value]));
23
+ }
24
+ function text(value, maximum = 4096) {
25
+ if (typeof value !== 'string' || !value.trim() || value.length > maximum || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(value)) fail('invalid-text');
26
+ return value;
27
+ }
28
+ function id(value) { if (typeof value !== 'string' || !ID.test(value)) fail('invalid-id'); return value; }
29
+ function boolean(value) { if (typeof value !== 'boolean') fail('invalid-boolean'); return value; }
30
+ function integer(value, minimum, maximum) {
31
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) fail('invalid-integer');
32
+ return value;
33
+ }
34
+ function array(value, limit, convert) {
35
+ if (!Array.isArray(value) || value.length > limit || Reflect.ownKeys(value).length !== value.length + 1) fail('invalid-array');
36
+ return Array.from({ length: value.length }, (_, index) => {
37
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
38
+ if (!descriptor || !Object.hasOwn(descriptor, 'value')) fail('invalid-array');
39
+ return convert(descriptor.value);
40
+ });
41
+ }
42
+ function unique(values) { if (new Set(values).size !== values.length) fail('duplicate-values'); return values; }
43
+ function path(value, glob = false) {
44
+ text(value, 512);
45
+ if (/[\\:\x00-\x1f\x7f]/.test(value) || value.startsWith('/') || value.endsWith('/')
46
+ || value.split('/').some(part => !part || part === '.' || part === '..')
47
+ || (glob && value.split('/').some(part => part.includes('**') && part !== '**'))) fail('invalid-path');
48
+ return value;
49
+ }
50
+ function freeze(value) {
51
+ if (value && typeof value === 'object') { for (const child of Object.values(value)) freeze(child); Object.freeze(value); }
52
+ return value;
53
+ }
54
+ function identity(value) {
55
+ const result = {};
56
+ if (!['plan', 'final'].includes(value.phase)) fail('invalid-phase');
57
+ result.phase = value.phase; result.runId = id(value.runId);
58
+ for (const key of ['requestDigest', 'planDigest', 'diffDigest']) {
59
+ if (typeof value[key] !== 'string' || !DIGEST.test(value[key])) fail('invalid-digest'); result[key] = value[key];
60
+ }
61
+ for (const key of ['baseSha', 'headSha']) {
62
+ if (typeof value[key] !== 'string' || !SHA.test(value[key])) fail('invalid-sha'); result[key] = value[key];
63
+ }
64
+ return result;
65
+ }
66
+ function captureSubject(input) {
67
+ const required = [...IDENTITY, 'acceptanceCriteria', 'planNodeIds', 'changedPaths', 'workerActorIds'];
68
+ const value = object(input, [...required, 'criterionPlanNodeIds'], required);
69
+ const criteria = array(value.acceptanceCriteria, 256, value => text(value));
70
+ if (!criteria.length) fail('missing-criteria');
71
+ const planNodeIds = unique(array(value.planNodeIds, 512, id));
72
+ let criterionPlanNodeIds;
73
+ if (value.criterionPlanNodeIds !== undefined) {
74
+ criterionPlanNodeIds = array(value.criterionPlanNodeIds, 256, entries => {
75
+ const nodes = unique(array(entries, 512, id));
76
+ if (!nodes.length || nodes.some(node => !planNodeIds.includes(node))) fail('invalid-criterion-plan-map');
77
+ return nodes;
78
+ });
79
+ if (criterionPlanNodeIds.length !== criteria.length) fail('invalid-criterion-plan-map');
80
+ }
81
+ return { ...identity(value), acceptanceCriteria: criteria, planNodeIds,
82
+ ...(criterionPlanNodeIds === undefined ? {} : { criterionPlanNodeIds }),
83
+ changedPaths: unique(array(value.changedPaths, 2048, value => path(value))),
84
+ workerActorIds: unique(array(value.workerActorIds, 512, id)) };
85
+ }
86
+ export function normalizeReviewPolicy(input) {
87
+ const value = input === undefined ? {} : object(input, ['required', 'maxRounds', 'reviewers', 'strictCoverage'], []);
88
+ const reviewers = array(value.reviewers ?? [], 64, item => {
89
+ const reviewer = object(item, ['id', 'paths', 'required']);
90
+ const paths = unique(array(reviewer.paths, 128, value => path(value, true)));
91
+ if (!paths.length) fail('empty-reviewer-paths');
92
+ return { id: id(reviewer.id), paths, required: boolean(reviewer.required) };
93
+ });
94
+ unique(reviewers.map(reviewer => reviewer.id));
95
+ return freeze({ required: boolean(value.required ?? false), maxRounds: integer(value.maxRounds ?? 3, 1, 10),
96
+ reviewers, strictCoverage: boolean(value.strictCoverage ?? false) });
97
+ }
98
+ // Segment matching avoids compiling project-controlled globs into backtracking regular expressions.
99
+ function matchSegment(pattern, value) {
100
+ let previous = new Array(value.length + 1).fill(false); previous[0] = true;
101
+ for (const token of pattern) {
102
+ const next = new Array(value.length + 1).fill(false);
103
+ if (token === '*') next[0] = previous[0];
104
+ for (let index = 1; index <= value.length; index += 1) {
105
+ next[index] = token === '*' ? previous[index] || next[index - 1]
106
+ : previous[index - 1] && (token === '?' || token === value[index - 1]);
107
+ }
108
+ previous = next;
109
+ }
110
+ return previous[value.length];
111
+ }
112
+ function matches(pattern, value) {
113
+ const segments = value.split('/');
114
+ let previous = new Array(segments.length + 1).fill(false); previous[0] = true;
115
+ for (const token of pattern.split('/')) {
116
+ const next = new Array(segments.length + 1).fill(false);
117
+ if (token === '**') next[0] = previous[0];
118
+ for (let index = 1; index <= segments.length; index += 1) {
119
+ next[index] = token === '**' ? previous[index] || next[index - 1]
120
+ : previous[index - 1] && matchSegment(token, segments[index - 1]);
121
+ }
122
+ previous = next;
123
+ }
124
+ return previous[segments.length];
125
+ }
126
+ function captureReport(subject, input, nowMs, requiredPaths) {
127
+ const value = object(input, [...IDENTITY, 'reviewerId', 'actorId', 'round', 'status', 'blocking', 'coverage',
128
+ 'findings', 'filesReviewed', 'commandsExecuted', 'checkedAt']);
129
+ const boundIdentity = identity(value);
130
+ if (IDENTITY.some(key => boundIdentity[key] !== subject[key])) fail('stale-review');
131
+ const actorId = id(value.actorId);
132
+ if (subject.workerActorIds.includes(actorId)) fail('self-review');
133
+ if (!['PASS', 'FAIL'].includes(value.status) || boolean(value.blocking) !== (value.status === 'FAIL')) fail('inconsistent-verdict');
134
+ const paths = entries => unique(array(entries, 2048, value => {
135
+ const result = path(value); if (!subject.changedPaths.includes(result)) fail('unknown-reviewed-path'); return result;
136
+ }));
137
+ const criterionIndex = index => integer(index, 1, subject.acceptanceCriteria.length);
138
+ const coverage = array(value.coverage, 256, entry => {
139
+ const item = object(entry, ['criterionIndex', 'planNodeIds', 'paths', 'evidence']);
140
+ const nodes = unique(array(item.planNodeIds, 512, value => {
141
+ const node = id(value); if (!subject.planNodeIds.includes(node)) fail('unknown-plan-node'); return node;
142
+ }));
143
+ const evidence = array(item.evidence, 64, entry => {
144
+ const item = object(entry, ['kind', 'reference', 'summary']);
145
+ if (!['test', 'manual'].includes(item.kind)) fail('invalid-evidence-kind');
146
+ return { kind: item.kind, reference: text(item.reference, 1024), summary: text(item.summary) };
147
+ });
148
+ if (!nodes.length || !evidence.length) fail('missing-coverage-evidence');
149
+ const mappedNodes = subject.criterionPlanNodeIds?.[criterionIndex(item.criterionIndex) - 1];
150
+ if (mappedNodes && nodes.some(node => !mappedNodes.includes(node))) fail('criterion-plan-node-mismatch');
151
+ return { criterionIndex: criterionIndex(item.criterionIndex), planNodeIds: nodes, paths: paths(item.paths), evidence };
152
+ });
153
+ unique(coverage.map(item => item.criterionIndex));
154
+ const findings = array(value.findings, 256, entry => {
155
+ const item = object(entry, ['kind', 'summary', 'blocking', 'criterionIndexes', 'paths'], ['kind', 'summary', 'blocking']);
156
+ if (!['missing', 'unrequested', 'general'].includes(item.kind)) fail('invalid-finding-kind');
157
+ return { kind: item.kind, summary: text(item.summary), blocking: boolean(item.blocking),
158
+ ...(item.criterionIndexes === undefined ? {} : { criterionIndexes: unique(array(item.criterionIndexes, 256, criterionIndex)) }),
159
+ ...(item.paths === undefined ? {} : { paths: paths(item.paths) }) };
160
+ });
161
+ const filesReviewed = paths(value.filesReviewed);
162
+ if (coverage.some(item => item.paths.some(path => !filesReviewed.includes(path)))) fail('coverage-file-not-reviewed');
163
+ if (value.status === 'PASS' && (coverage.length !== subject.acceptanceCriteria.length
164
+ || requiredPaths.some(path => !filesReviewed.includes(path)) || findings.some(item => item.blocking))) fail('incomplete-pass');
165
+ const checkedAt = text(value.checkedAt, 32), checkedTime = Date.parse(checkedAt);
166
+ if (!Number.isSafeInteger(nowMs) || nowMs < 0 || !Number.isFinite(checkedTime)
167
+ || new Date(checkedTime).toISOString() !== checkedAt || checkedTime > nowMs) fail('invalid-review-time');
168
+ const result = { ...boundIdentity, reviewerId: id(value.reviewerId), actorId, round: integer(value.round, 1, 10),
169
+ status: value.status, blocking: value.blocking, coverage, findings, filesReviewed,
170
+ commandsExecuted: array(value.commandsExecuted, 128, value => text(value)), checkedAt };
171
+ if (Buffer.byteLength(JSON.stringify(result)) > 512 * 1024) fail('review-too-large');
172
+ return result;
173
+ }
174
+ // These contracts authenticate supplied identity and structure, not semantic truth or command execution.
175
+ export function validateReviewReport(subjectInput, report, { nowMs = Date.now() } = {}) {
176
+ const subject = captureSubject(subjectInput);
177
+ return freeze(captureReport(subject, report, nowMs, subject.changedPaths));
178
+ }
179
+ export function evaluateReviewReports(policyInput, subjectInput, reportInputs, { nowMs = Date.now() } = {}) {
180
+ const policy = normalizeReviewPolicy(policyInput), subject = captureSubject(subjectInput);
181
+ const selected = new Map(), uncovered = [];
182
+ for (const changedPath of subject.changedPaths) {
183
+ const matchesPath = policy.reviewers.filter(reviewer => reviewer.paths.some(pattern => matches(pattern, changedPath)));
184
+ if (!matchesPath.length) {
185
+ if (policy.strictCoverage) uncovered.push(changedPath);
186
+ else {
187
+ if (!selected.has('general')) selected.set('general', { required: true, paths: [] });
188
+ selected.get('general').required = true;
189
+ selected.get('general').paths.push(changedPath);
190
+ }
191
+ }
192
+ for (const reviewer of matchesPath) {
193
+ if (!selected.has(reviewer.id)) selected.set(reviewer.id, { required: reviewer.required, paths: [] });
194
+ selected.get(reviewer.id).paths.push(changedPath);
195
+ }
196
+ }
197
+ if (!subject.changedPaths.length) selected.set('general', { required: true, paths: [] });
198
+ const requiredReviewerIds = policy.required ? [...selected].filter(([, value]) => value.required).map(([key]) => key).sort() : [];
199
+ const reports = array(reportInputs, 650, value => {
200
+ const reviewerId = Object.getOwnPropertyDescriptor(value ?? {}, 'reviewerId')?.value;
201
+ if (!selected.has(reviewerId)) fail('unknown-reviewer');
202
+ const report = captureReport(subject, value, nowMs, selected.get(reviewerId).paths);
203
+ if (report.round > policy.maxRounds) fail('round-limit');
204
+ return report;
205
+ });
206
+ unique(reports.map(report => `${report.reviewerId}:${report.round}`));
207
+ const rounds = Math.max(0, ...reports.map(report => report.round));
208
+ const latest = new Map();
209
+ for (const report of reports) if (!latest.has(report.reviewerId) || latest.get(report.reviewerId).round < report.round) latest.set(report.reviewerId, report);
210
+ const blockers = policy.required ? uncovered.map(path => ({ code: 'uncovered-path', path })) : [];
211
+ for (const reviewerId of requiredReviewerIds) {
212
+ const report = latest.get(reviewerId);
213
+ if (!report) blockers.push({ code: 'missing-review', reviewerId });
214
+ else if (report.blocking) blockers.push({ code: 'blocking-review', reviewerId, round: report.round });
215
+ }
216
+ if (policy.required) {
217
+ const reviewed = new Set([...latest.values()].filter(report => report.status === 'PASS').flatMap(report => report.filesReviewed));
218
+ for (const path of subject.changedPaths) if (!reviewed.has(path)) blockers.push({ code: 'unreviewed-path', path });
219
+ }
220
+ // An explicitly submitted blocking finding is never erased by marking its reviewer optional.
221
+ for (const [reviewerId, report] of latest) if (report.blocking && !requiredReviewerIds.includes(reviewerId)) blockers.push({ code: 'blocking-review', reviewerId, round: report.round });
222
+ return freeze({ valid: blockers.length === 0, requiredReviewerIds,
223
+ reviewerPaths: Object.fromEntries([...selected].map(([key, value]) => [key, unique(value.paths).sort()])), rounds,
224
+ humanEscalation: blockers.length > 0 && rounds >= policy.maxRounds, blockers, reports });
225
+ }
@@ -0,0 +1,120 @@
1
+ import { types } from 'node:util';
2
+ import { containsSecretMaterial } from '../clients/contract.js';
3
+ const RULES = ['private-key', 'github-token', 'npm-token', 'aws-access-key', 'unicode-bidi', 'unicode-mixed-identifier', 'type-suppression'];
4
+ const DEFAULTS = { secrets: true, unicode: 'warn', maxFiles: 1000, maxBytes: 2097152, allowlist: [], shortcutRules: [] };
5
+ function fail(code) { const error = new Error('Content scan input, policy or resource limit is invalid.'); error.code = code; throw error; }
6
+ // Capture data properties only. Do not evaluate accessors or custom objects.
7
+ function capture(input, code) {
8
+ const active = new WeakSet(); let nodes = 0, bytes = 0;
9
+ function clone(value, depth = 0) {
10
+ if (++nodes > 16000 || depth > 8) fail(code);
11
+ if (value === undefined || value === null || typeof value === 'boolean') return value;
12
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
13
+ if (typeof value === 'string') {
14
+ if (value.length > 3 * 1024 * 1024) fail(code);
15
+ bytes += Buffer.byteLength(value, 'utf8');
16
+ if (bytes > 3 * 1024 * 1024) fail(code);
17
+ return value;
18
+ }
19
+ if (!value || typeof value !== 'object' || types.isProxy(value) || active.has(value)) fail(code);
20
+ const array = Array.isArray(value), prototype = Object.getPrototypeOf(value);
21
+ if (array ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null) fail(code);
22
+ active.add(value);
23
+ try {
24
+ const keys = Reflect.ownKeys(value);
25
+ const length = array ? Object.getOwnPropertyDescriptor(value, 'length')?.value : 0;
26
+ if (keys.length > 1001 || (array && (!Number.isInteger(length) || length > 1000 || keys.length !== length + 1))) fail(code);
27
+ const output = array ? [] : Object.create(null);
28
+ for (const key of keys) {
29
+ if (array && key === 'length') continue;
30
+ if (typeof key !== 'string' || (array && (!/^(0|[1-9][0-9]*)$/.test(key) || Number(key) >= length))) fail(code);
31
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
32
+ if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) fail(code);
33
+ output[key] = clone(descriptor.value, depth + 1);
34
+ }
35
+ return Object.freeze(output);
36
+ } finally { active.delete(value); }
37
+ }
38
+ try { return clone(input); } catch { fail(code); }
39
+ }
40
+ function object(value, keys) { return value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).every(key => keys.includes(key)); }
41
+ function pathValid(value) { return typeof value === 'string' && value.length > 0 && value.length <= 1024 && !/[\\*?\x00-\x1f\x7f]/.test(value) && !value.startsWith('/') && !value.split('/').some(part => !part || part === '.' || part === '..') && !/^[A-Za-z]:/.test(value); }
42
+ function reasonValid(value) { return typeof value === 'string' && value.trim().length > 0 && value.length <= 500 && !/[\p{Cc}\p{Cf}]/u.test(value) && !containsSecretMaterial(value) && !/npm_[A-Za-z0-9]{36}|-----BEGIN .*PRIVATE KEY-----/.test(value); }
43
+ export function validateContentScanPolicy(input = {}) {
44
+ input = capture(input, 'ERR_CONTENT_SCAN_POLICY');
45
+ const bad = () => fail('ERR_CONTENT_SCAN_POLICY');
46
+ if (!object(input, Object.keys(DEFAULTS))) bad();
47
+ const policy = { ...DEFAULTS, ...input };
48
+ if (typeof policy.secrets !== 'boolean' || !['warn', 'required', 'off'].includes(policy.unicode)) bad();
49
+ if (!Number.isInteger(policy.maxFiles) || policy.maxFiles < 1 || policy.maxFiles > 1000 || !Number.isInteger(policy.maxBytes) || policy.maxBytes < 1 || policy.maxBytes > 2097152) bad();
50
+ if (!Array.isArray(policy.allowlist) || policy.allowlist.length > 100 || !Array.isArray(policy.shortcutRules) || policy.shortcutRules.length > 100) bad();
51
+ const seen = new Set();
52
+ for (const entry of policy.allowlist) {
53
+ if (!object(entry, ['ruleId', 'path', 'reason']) || !RULES.includes(entry.ruleId) || !pathValid(entry.path) || !reasonValid(entry.reason)) bad();
54
+ const key = `${entry.ruleId}:${entry.path}`; if (seen.has(key)) bad(); seen.add(key);
55
+ }
56
+ for (const entry of policy.shortcutRules) {
57
+ if (!object(entry, ['ruleId', 'paths', 'reason', 'required']) || entry.ruleId !== 'type-suppression' || !reasonValid(entry.reason) || (entry.required !== undefined && typeof entry.required !== 'boolean') || !Array.isArray(entry.paths) || !entry.paths.length || entry.paths.length > 100 || !entry.paths.every(pathValid) || new Set(entry.paths).size !== entry.paths.length) bad();
58
+ }
59
+ return capture(policy, 'ERR_CONTENT_SCAN_POLICY');
60
+ }
61
+
62
+ // Mask ordinary strings and comments before examining code identifiers. This is a
63
+ // conservative lexical heuristic, not a parser or a substitute for a compiler.
64
+ function codeOnly(text) {
65
+ return text.replace(/"""[\s\S]*?(?:"""|$)|'''[\s\S]*?(?:'''|$)|\/\*[\s\S]*?(?:\*\/|$)|\/\/[^\n]*|#[^\n]*|"(?:\\[\s\S]|[^"\\])*(?:"|$)|'(?:\\[\s\S]|[^'\\])*(?:'|$)|`(?:\\[\s\S]|[^`\\])*(?:`|$)/g, match => match.replace(/[^\n]/g, ' '));
66
+ }
67
+ const CODE = /\.(?:[cm]?[jt]sx?|py|go|rs|java|kt|kts|c|h|cpp|hpp|cc|cs|swift|rb|php|scala)$/i;
68
+ const SECRET_RULES = [
69
+ ['private-key', /-----BEGIN (?:RSA |EC |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY-----/g, 'Private key material detected.'],
70
+ ['github-token', /\b(?:gh[pousr]_[A-Za-z0-9]{36,255}|github_pat_[A-Za-z0-9_]{40,255})\b/g, 'GitHub token pattern detected.'],
71
+ ['npm-token', /\bnpm_[A-Za-z0-9]{36}\b/g, 'npm token pattern detected.'],
72
+ ['aws-access-key', /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, 'AWS access key identifier detected.'],
73
+ ];
74
+ export function scanChangedContent(input = {}) {
75
+ const captured = capture(input, 'ERR_CONTENT_SCAN_INPUT');
76
+ if (!object(captured, ['files', 'policy'])) fail('ERR_CONTENT_SCAN_INPUT');
77
+ const { files } = captured;
78
+ const policy = validateContentScanPolicy(captured.policy);
79
+ if (!Array.isArray(files)) fail('ERR_CONTENT_SCAN_INPUT');
80
+ if (files.length > policy.maxFiles) fail('ERR_CONTENT_SCAN_LIMIT');
81
+ let bytes = 0;
82
+ const paths = new Set();
83
+ for (const file of files) {
84
+ if (!object(file, ['path', 'content']) || !pathValid(file.path) || typeof file.content !== 'string' || paths.has(file.path)) fail('ERR_CONTENT_SCAN_INPUT');
85
+ paths.add(file.path); bytes += Buffer.byteLength(file.content, 'utf8');
86
+ if (bytes > policy.maxBytes) fail('ERR_CONTENT_SCAN_LIMIT');
87
+ }
88
+ const findings = [];
89
+ for (const file of files) {
90
+ const lineStarts = [0];
91
+ for (let index = 0; index < file.content.length; index += 1) if (file.content[index] === '\n') lineStarts.push(index + 1);
92
+ const lineAt = index => {
93
+ let low = 0, high = lineStarts.length;
94
+ while (low < high) { const middle = (low + high) >>> 1; if (lineStarts[middle] <= index) low = middle + 1; else high = middle; }
95
+ return low;
96
+ };
97
+ const add = (ruleId, index, severity, message) => {
98
+ if (policy.allowlist.some(entry => entry.path === file.path && entry.ruleId === ruleId)) return;
99
+ // Paths are the only source-derived string; suppress terminal control bytes.
100
+ const path = file.path.replace(/[\u202a-\u202e\u2066-\u2069]/g, char => `\\u${char.charCodeAt(0).toString(16)}`);
101
+ findings.push({ path, line: lineAt(index), ruleId, severity, message });
102
+ if (findings.length > 1000) fail('ERR_CONTENT_SCAN_LIMIT');
103
+ };
104
+ if (policy.secrets) for (const [rule, pattern, message] of SECRET_RULES) {
105
+ for (const match of file.content.matchAll(new RegExp(pattern.source, pattern.flags))) add(rule, match.index, 'error', message);
106
+ }
107
+ if (policy.unicode !== 'off') {
108
+ const severity = policy.unicode === 'required' ? 'error' : 'warning';
109
+ for (const match of file.content.matchAll(/[\u202a-\u202e\u2066-\u2069]/g)) add('unicode-bidi', match.index, severity, 'Bidirectional text control requires review.');
110
+ if (CODE.test(file.path)) for (const match of codeOnly(file.content).matchAll(/[\p{L}_$][\p{L}\p{M}\p{N}_$]*/gu)) {
111
+ const scripts = [/\p{Script=Latin}/u, /\p{Script=Cyrillic}/u, /\p{Script=Greek}/u].filter(pattern => pattern.test(match[0]));
112
+ if (scripts.length > 1) add('unicode-mixed-identifier', match.index, severity, 'Identifier mixes Latin, Cyrillic or Greek scripts; review for confusables.');
113
+ }
114
+ }
115
+ for (const rule of policy.shortcutRules) if (rule.paths.includes(file.path)) {
116
+ for (const match of file.content.matchAll(/@ts-(?:ignore|nocheck)\b/g)) add('type-suppression', match.index, rule.required ? 'error' : 'warning', 'Configured TypeScript suppression rule requires review.');
117
+ }
118
+ }
119
+ return capture({ status: findings.some(finding => finding.severity === 'error') ? 'failed' : 'passed', findings, scannedFiles: files.length, bytes }, 'ERR_CONTENT_SCAN_INPUT');
120
+ }
@@ -206,6 +206,16 @@ export async function resolveExistingStatePaths(projectRoot, instance, options =
206
206
  }));
207
207
  }
208
208
 
209
+ export function governancePaths(featurePaths) {
210
+ assertResolvedStatePaths(featurePaths);
211
+ if (featurePaths.runDir !== featurePaths.instanceDir) throw new TypeError('Invalid feature run paths');
212
+ return registerPaths(Object.freeze({
213
+ ...featurePaths,
214
+ snapshotPath: join(featurePaths.runDir, 'governance.json'),
215
+ lockPath: join(featurePaths.runDir, 'governance.lock'),
216
+ }));
217
+ }
218
+
209
219
  export function verificationReportPaths(featurePaths) {
210
220
  assertResolvedStatePaths(featurePaths);
211
221
  if (featurePaths.runDir !== featurePaths.instanceDir) throw new TypeError('Invalid feature run paths');
@@ -1,4 +1,5 @@
1
1
  # Opt-in example: review and copy to your project's .github/workflows/rivet-deploy.yml.
2
+ # JavaScript actions require Actions Runner 2.327.1 or newer (GitHub-hosted runners provide this).
2
3
  # Configure the staging environment's protection rules and implement trusted project
3
4
  # scripts `deploy` and `verify:deployment`. Verification must fail on unhealthy delivery.
4
5
  # Rivet creates deployments using a PAT or GitHub App token. Deployments created by
@@ -28,7 +29,7 @@ jobs:
28
29
  steps:
29
30
  - name: Validate deployment contract before executing project code
30
31
  id: validate
31
- uses: actions/github-script@v7
32
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
32
33
  with:
33
34
  script: |
34
35
  const deployment = context.payload.deployment;
@@ -47,7 +48,7 @@ jobs:
47
48
  }
48
49
  - name: Record deployment started
49
50
  id: status-start
50
- uses: actions/github-script@v7
51
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
51
52
  with:
52
53
  script: |
53
54
  await github.rest.repos.createDeploymentStatus({
@@ -57,13 +58,14 @@ jobs:
57
58
  log_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
58
59
  });
59
60
  - name: Checkout the requested merge commit
60
- uses: actions/checkout@v4
61
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
61
62
  with:
62
63
  ref: ${{ github.event.deployment.sha }}
63
64
  persist-credentials: false
64
65
  - name: Set up Node
65
- uses: actions/setup-node@v4
66
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
66
67
  with:
68
+ package-manager-cache: false
67
69
  node-version: '22'
68
70
  - name: Install project dependencies
69
71
  run: npm ci
@@ -74,7 +76,7 @@ jobs:
74
76
  - name: Record verified deployment success
75
77
  id: status-success
76
78
  if: success()
77
- uses: actions/github-script@v7
79
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
78
80
  with:
79
81
  script: |
80
82
  await github.rest.repos.createDeploymentStatus({
@@ -87,7 +89,7 @@ jobs:
87
89
  - name: Record deployment or verification failure
88
90
  id: status-failure
89
91
  if: failure() && steps.validate.outcome == 'success'
90
- uses: actions/github-script@v7
92
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
91
93
  with:
92
94
  script: |
93
95
  await github.rest.repos.createDeploymentStatus({
@@ -13,7 +13,7 @@ Use `rivet --help --advanced` to see the complete installed command reference. F
13
13
 
14
14
  ## Connect the application project
15
15
 
16
- Node.js 22 or 24 runs Rivet; the application can use Python, Django, Go, Rust, Node.js, or another language. Do not add a dummy `package.json` or npm scripts to make a non-Node project pass setup. Preview with `rivet setup --project=<path>` and review the proposed checks before `--write`. Setup proposes conventional checks for root Django `manage.py`, Python metadata identifying pytest, Go modules, and Cargo projects.
16
+ Node.js 22 or 24 runs Rivet; the application can use Python, Django, Go, Rust, Node.js, or another language. Use the application’s native configuration and verification commands. Preview with `rivet setup --project=<path>` and review the proposed checks before `--write`. Setup proposes conventional checks for root Django `manage.py`, Python metadata identifying pytest, Go modules, and Cargo projects.
17
17
 
18
18
  If no checks are detected, determine the project's real verification command from its documentation and configuration. Interactive `rivet setup --write` can ask the user for that command. In a harness or noninteractive session, supply exact arguments, for example `--checks-json='{"test":["python3","-m","pytest"]}'`, with `--project=<path>` and, after review, `--write`. Do not invent a successful placeholder check. Schema 3 uses command groups with `steps` containing `cwd` and `argv`; quality policy must require at least one `test` or `check`, while `build` is optional. Arguments run directly without shell expansion or chaining. Existing valid project policy is preserved by setup; edit its project and quality files together when changing checks.
19
19
 
@@ -88,3 +88,11 @@ For Jira/Linear with harness-connected tools, inspect `rivet integrations list/c
88
88
 
89
89
 
90
90
  For troubleshooting, human-readable failures print a private diagnostic report path when saving succeeds. `rivet support --save` collects broader sanitized diagnostics. JSON automation keeps its existing response contract and does not save automatic failure reports; `support --save --json` is explicit. Ask the user to review a report before attaching it to an issue. Never upload reports, raw terminal logs, private state or source code without authorization.
91
+
92
+ ## Decisions, review and evidence
93
+
94
+ Record material decisions and alternatives through `rivet task decide --input-json=<serialized-decision>` and inspect them with `rivet task decisions`. A pending human decision requires the user's interactive `rivet task approve-decision --decision=<id>`; do not simulate approval. Decisions never expand the sealed task authority. Inline JSON is bounded to 64 KiB. Alternatively use `--input` with an existing project-contained private/ignored JSON file up to 128 KiB; never combine the options or create ordinary untracked input files that dirty the approved baseline.
95
+
96
+ Export current plan or final review context using `rivet task review --phase=plan|final`. Arrange the required independent review through the approved harness or team process, then submit the structured report using `rivet task review --input-json=<serialized-report>`. Rivet validates recorded review identity and configured coverage; it does not automatically launch or authenticate a separate reviewer. Re-review when the task, plan or accepted changes differ. Do not invent findings, reviewer identities or a passing judgment to bypass a required review or round limit.
97
+
98
+ Preserve the provenance of every numerical claim: command or measurement method, source commit, environment, time, units and artifact. Distinguish observations from estimates and provider telemetry from model-reported usage. Demonstrate detection with safe known-bad fixtures and positive controls when practical. Include affected public documentation in the same pull request. Treat scan findings and unavailable evidence as work to resolve; do not broadly suppress rules or report unavailable context-window telemetry as measured fact.