@guilz-dev/belay 0.2.0 → 0.4.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/README.md +39 -12
- package/dist/adapters/cursor/runtime-entry.js +1 -0
- package/dist/adapters/shared/gate-runtime.d.ts +11 -14
- package/dist/adapters/shared/gate-runtime.js +289 -23
- package/dist/bundle/claude-runtime.mjs +4435 -708
- package/dist/bundle/codex-runtime.mjs +4448 -710
- package/dist/bundle/cursor-runtime.mjs +4437 -710
- package/dist/cli.js +333 -11
- package/dist/commands/approve.d.ts +1 -0
- package/dist/commands/approve.js +56 -26
- package/dist/commands/config.d.ts +70 -0
- package/dist/commands/config.js +577 -0
- package/dist/commands/doctor.js +13 -2
- package/dist/commands/explain.js +6 -1
- package/dist/commands/harvest.d.ts +29 -0
- package/dist/commands/harvest.js +72 -0
- package/dist/commands/health-snapshot.d.ts +6 -0
- package/dist/commands/health-snapshot.js +34 -0
- package/dist/commands/judge.d.ts +100 -0
- package/dist/commands/judge.js +304 -0
- package/dist/commands/metrics.js +34 -2
- package/dist/commands/quality.d.ts +38 -0
- package/dist/commands/quality.js +105 -0
- package/dist/commands/simulate.d.ts +1 -0
- package/dist/commands/simulate.js +7 -2
- package/dist/commands/standing-allow.d.ts +10 -0
- package/dist/commands/standing-allow.js +26 -0
- package/dist/commands/status.js +2 -2
- package/dist/commands/stdin-key.d.ts +1 -0
- package/dist/commands/stdin-key.js +8 -0
- package/dist/commands/tui.d.ts +48 -0
- package/dist/commands/tui.js +150 -0
- package/dist/config-io.js +10 -5
- package/dist/conformance/guarantee-table.js +12 -0
- package/dist/conformance/types.d.ts +2 -0
- package/dist/core/approval-replay-cli.d.ts +7 -0
- package/dist/core/approval-replay-cli.js +36 -0
- package/dist/core/approval-replay.d.ts +45 -0
- package/dist/core/approval-replay.js +141 -0
- package/dist/core/approval-service.d.ts +15 -0
- package/dist/core/approval-service.js +47 -2
- package/dist/core/approval.d.ts +24 -3
- package/dist/core/approval.js +47 -4
- package/dist/core/audit-analysis.d.ts +7 -1
- package/dist/core/audit-analysis.js +111 -0
- package/dist/core/audit-io.d.ts +2 -0
- package/dist/core/audit-io.js +14 -0
- package/dist/core/audit-metrics.d.ts +7 -0
- package/dist/core/audit-metrics.js +24 -4
- package/dist/core/audit-query.d.ts +1 -0
- package/dist/core/audit-query.js +3 -0
- package/dist/core/audit-replay-context.d.ts +35 -0
- package/dist/core/audit-replay-context.js +88 -0
- package/dist/core/audit-types.d.ts +24 -1
- package/dist/core/audit-types.js +1 -1
- package/dist/core/capability/index.d.ts +4 -3
- package/dist/core/capability/index.js +3 -2
- package/dist/core/capability/paths.d.ts +2 -1
- package/dist/core/capability/paths.js +76 -8
- package/dist/core/capability/reasons.d.ts +3 -0
- package/dist/core/capability/reasons.js +8 -0
- package/dist/core/capability/trusted-workspace-roots.d.ts +25 -0
- package/dist/core/capability/trusted-workspace-roots.js +149 -0
- package/dist/core/capability/types.d.ts +11 -1
- package/dist/core/capability-approval.d.ts +2 -1
- package/dist/core/capability-approval.js +100 -2
- package/dist/core/classify-subagent.js +2 -20
- package/dist/core/classify-tool.js +129 -30
- package/dist/core/config-layers.js +2 -1
- package/dist/core/config.d.ts +40 -2
- package/dist/core/config.js +159 -11
- package/dist/core/credential-store.d.ts +11 -0
- package/dist/core/credential-store.js +60 -0
- package/dist/core/gate-engine.js +104 -13
- package/dist/core/harvest.d.ts +53 -0
- package/dist/core/harvest.js +276 -0
- package/dist/core/index.d.ts +9 -3
- package/dist/core/index.js +6 -2
- package/dist/core/integrity.d.ts +2 -0
- package/dist/core/integrity.js +13 -0
- package/dist/core/judge-api-key.d.ts +19 -0
- package/dist/core/judge-api-key.js +74 -0
- package/dist/core/judge-cloud-consent.d.ts +13 -0
- package/dist/core/judge-cloud-consent.js +38 -0
- package/dist/core/judge-config.d.ts +41 -4
- package/dist/core/judge-config.js +263 -57
- package/dist/core/judge-doctor.d.ts +9 -1
- package/dist/core/judge-doctor.js +199 -94
- package/dist/core/judge-model-discovery.d.ts +24 -0
- package/dist/core/judge-model-discovery.js +168 -0
- package/dist/core/judge-model-policy.d.ts +5 -0
- package/dist/core/judge-model-policy.js +21 -0
- package/dist/core/judge-runtime-detection.d.ts +9 -0
- package/dist/core/judge-runtime-detection.js +68 -0
- package/dist/core/path-utils.d.ts +11 -0
- package/dist/core/path-utils.js +56 -6
- package/dist/core/reclassify.d.ts +3 -0
- package/dist/core/reclassify.js +44 -14
- package/dist/core/replay-scrub.d.ts +9 -0
- package/dist/core/replay-scrub.js +43 -0
- package/dist/core/shell-tokenizer.d.ts +2 -0
- package/dist/core/shell-tokenizer.js +82 -23
- package/dist/core/standing-allow.d.ts +50 -0
- package/dist/core/standing-allow.js +175 -0
- package/dist/core/transactional/diff-evaluator.js +1 -19
- package/dist/core/types.d.ts +17 -0
- package/dist/core/verdict/adapter.d.ts +1 -0
- package/dist/core/verdict/adapter.js +17 -2
- package/dist/core/verdict/containment.d.ts +8 -2
- package/dist/core/verdict/containment.js +45 -10
- package/dist/core/verdict/judge-audit.d.ts +1 -0
- package/dist/core/verdict/judge-audit.js +32 -1
- package/dist/core/verdict/judge-baseline.d.ts +20 -0
- package/dist/core/verdict/judge-baseline.js +48 -0
- package/dist/core/verdict/judge-broker-service.d.ts +42 -0
- package/dist/core/verdict/judge-broker-service.js +279 -0
- package/dist/core/verdict/judge-catalog.d.ts +40 -0
- package/dist/core/verdict/judge-catalog.js +148 -0
- package/dist/core/verdict/judge-cli-fingerprint.d.ts +5 -0
- package/dist/core/verdict/judge-cli-fingerprint.js +61 -0
- package/dist/core/verdict/judge-cli.d.ts +46 -0
- package/dist/core/verdict/judge-cli.js +401 -0
- package/dist/core/verdict/judge-factory.d.ts +15 -4
- package/dist/core/verdict/judge-factory.js +127 -15
- package/dist/core/verdict/judge-provider-matrix.d.ts +20 -0
- package/dist/core/verdict/judge-provider-matrix.js +66 -0
- package/dist/core/verdict/judge-runtime-config.d.ts +42 -0
- package/dist/core/verdict/judge-runtime-config.js +92 -0
- package/dist/core/verdict/judge-session-broker.d.ts +48 -0
- package/dist/core/verdict/judge-session-broker.js +195 -0
- package/dist/core/verdict/judge-session-guard.d.ts +27 -0
- package/dist/core/verdict/judge-session-guard.js +91 -0
- package/dist/core/verdict/judge-session-kill-switch.d.ts +11 -0
- package/dist/core/verdict/judge-session-kill-switch.js +43 -0
- package/dist/core/verdict/judge-session-mutex.d.ts +9 -0
- package/dist/core/verdict/judge-session-mutex.js +23 -0
- package/dist/core/verdict/judge-shadow.d.ts +24 -0
- package/dist/core/verdict/judge-shadow.js +116 -0
- package/dist/core/verdict/judge-transport.d.ts +40 -0
- package/dist/core/verdict/judge-transport.js +258 -0
- package/dist/core/verdict/judge.d.ts +33 -1
- package/dist/core/verdict/judge.js +84 -14
- package/dist/core/verdict/parser.js +6 -1
- package/dist/core/verdict/persistent-paths.d.ts +8 -0
- package/dist/core/verdict/persistent-paths.js +52 -0
- package/dist/core/verdict/types.d.ts +19 -2
- package/dist/core/verdict/verdict.js +166 -49
- package/dist/corpus/adversarial-probe.d.ts +86 -0
- package/dist/corpus/adversarial-probe.js +220 -0
- package/dist/corpus/evaluate.d.ts +45 -12
- package/dist/corpus/evaluate.js +62 -5
- package/dist/corpus/gates.d.ts +35 -0
- package/dist/corpus/gates.js +81 -0
- package/dist/corpus/judge-accuracy.d.ts +8 -0
- package/dist/corpus/judge-accuracy.js +39 -0
- package/dist/corpus/must-allow-commands.d.ts +5 -0
- package/dist/corpus/must-allow-commands.js +13 -0
- package/dist/corpus/mutators.d.ts +28 -0
- package/dist/corpus/mutators.js +151 -0
- package/dist/corpus/ratchet.d.ts +42 -0
- package/dist/corpus/ratchet.js +116 -0
- package/dist/corpus/runtime-match.d.ts +19 -0
- package/dist/corpus/runtime-match.js +45 -0
- package/dist/corpus/standing-allow-catalog.generated.d.ts +13 -0
- package/dist/corpus/standing-allow-catalog.generated.js +79 -0
- package/dist/corpus/types.d.ts +47 -0
- package/dist/corpus/types.js +157 -0
- package/dist/installer.js +69 -15
- package/dist/judge-broker-daemon.d.ts +1 -0
- package/dist/judge-broker-daemon.js +123 -0
- package/dist/services/sandbox-service.d.ts +1 -0
- package/dist/services/sandbox-service.js +2 -0
- package/dist/types.d.ts +7 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -3
- package/skills/belay/SKILL.md +15 -8
- package/skills/belay/belay-approve.md +17 -0
- package/dist/commands/init-wizard.d.ts +0 -21
- package/dist/commands/init-wizard.js +0 -63
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { buildTier1Prompt, parseTier1Json, prescanInterpreterCode, } from './judge.js';
|
|
3
|
+
import { scrubOutboundForJudge } from './judge-outbound.js';
|
|
4
|
+
import { DEFAULT_JUDGE_RUNTIME_CONFIG } from './judge-runtime-config.js';
|
|
5
|
+
import { transportFallbackToFailClosedReason } from './judge-session-guard.js';
|
|
6
|
+
import { evaluateWithJudgeTransport } from './judge-transport.js';
|
|
7
|
+
export class CliRunError extends Error {
|
|
8
|
+
kind;
|
|
9
|
+
exitCode;
|
|
10
|
+
stderr;
|
|
11
|
+
stdout;
|
|
12
|
+
command;
|
|
13
|
+
constructor(kind, message, details = {}) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'CliRunError';
|
|
16
|
+
this.kind = kind;
|
|
17
|
+
this.exitCode = details.exitCode;
|
|
18
|
+
this.stderr = details.stderr;
|
|
19
|
+
this.stdout = details.stdout;
|
|
20
|
+
this.command = details.command;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const CLI_BINARIES = {
|
|
24
|
+
codex: 'codex',
|
|
25
|
+
cursor: 'cursor-agent',
|
|
26
|
+
claude: 'claude',
|
|
27
|
+
};
|
|
28
|
+
const FORCE_KILL_GRACE_MS = 250;
|
|
29
|
+
function failClosedVerdict(reason) {
|
|
30
|
+
return {
|
|
31
|
+
local_recoverable: false,
|
|
32
|
+
destroys_outside_repo: true,
|
|
33
|
+
destroys_history_or_secrets: true,
|
|
34
|
+
external_change: true,
|
|
35
|
+
reason,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function transportForProvider(providerId) {
|
|
39
|
+
return `${providerId}-cli`;
|
|
40
|
+
}
|
|
41
|
+
function traceProvider(providerId) {
|
|
42
|
+
if (providerId === 'claude') {
|
|
43
|
+
return 'anthropic';
|
|
44
|
+
}
|
|
45
|
+
return 'openai-compatible';
|
|
46
|
+
}
|
|
47
|
+
function initialTrace(options) {
|
|
48
|
+
return {
|
|
49
|
+
provider: traceProvider(options.providerId),
|
|
50
|
+
modelRequested: options.modelRequested,
|
|
51
|
+
modelResolved: options.modelResolved,
|
|
52
|
+
latencyMs: 0,
|
|
53
|
+
transport: transportForProvider(options.providerId),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function buildCliInvocation(providerId, prompt, model) {
|
|
57
|
+
if (providerId === 'codex') {
|
|
58
|
+
return {
|
|
59
|
+
binary: CLI_BINARIES[providerId],
|
|
60
|
+
args: [
|
|
61
|
+
'exec',
|
|
62
|
+
'--json',
|
|
63
|
+
'--skip-git-repo-check',
|
|
64
|
+
'--sandbox',
|
|
65
|
+
'read-only',
|
|
66
|
+
'--model',
|
|
67
|
+
model,
|
|
68
|
+
'-',
|
|
69
|
+
],
|
|
70
|
+
stdin: prompt,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (providerId === 'cursor') {
|
|
74
|
+
return {
|
|
75
|
+
binary: CLI_BINARIES[providerId],
|
|
76
|
+
args: [
|
|
77
|
+
'--print',
|
|
78
|
+
'--output-format',
|
|
79
|
+
'json',
|
|
80
|
+
'--mode',
|
|
81
|
+
'ask',
|
|
82
|
+
'--sandbox',
|
|
83
|
+
'enabled',
|
|
84
|
+
'--trust',
|
|
85
|
+
'--model',
|
|
86
|
+
model,
|
|
87
|
+
prompt,
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
binary: CLI_BINARIES[providerId],
|
|
93
|
+
args: [
|
|
94
|
+
'-p',
|
|
95
|
+
'--output-format',
|
|
96
|
+
'json',
|
|
97
|
+
'--permission-mode',
|
|
98
|
+
'plan',
|
|
99
|
+
'--tools',
|
|
100
|
+
'',
|
|
101
|
+
'--bare',
|
|
102
|
+
'--model',
|
|
103
|
+
model,
|
|
104
|
+
],
|
|
105
|
+
stdin: prompt,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function tryParseJson(raw) {
|
|
109
|
+
try {
|
|
110
|
+
return JSON.parse(raw);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function extractVerdictFromJsonValue(value) {
|
|
117
|
+
if (typeof value === 'string') {
|
|
118
|
+
return parseTier1Json(value.trim());
|
|
119
|
+
}
|
|
120
|
+
if (Array.isArray(value)) {
|
|
121
|
+
for (let index = value.length - 1; index >= 0; index -= 1) {
|
|
122
|
+
const verdict = extractVerdictFromJsonValue(value[index]);
|
|
123
|
+
if (verdict) {
|
|
124
|
+
return verdict;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
if (!value || typeof value !== 'object') {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const record = value;
|
|
133
|
+
for (const key of ['result', 'content', 'text', 'message', 'output_text', 'final']) {
|
|
134
|
+
if (!(key in record)) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const verdict = extractVerdictFromJsonValue(record[key]);
|
|
138
|
+
if (verdict) {
|
|
139
|
+
return verdict;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (const candidate of Object.values(record).reverse()) {
|
|
143
|
+
const verdict = extractVerdictFromJsonValue(candidate);
|
|
144
|
+
if (verdict) {
|
|
145
|
+
return verdict;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
function parseCliJsonLines(raw) {
|
|
151
|
+
const lines = raw
|
|
152
|
+
.split(/\r?\n/)
|
|
153
|
+
.map((line) => line.trim())
|
|
154
|
+
.filter(Boolean);
|
|
155
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
156
|
+
const parsed = tryParseJson(lines[index] ?? '');
|
|
157
|
+
if (!parsed) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const verdict = extractVerdictFromJsonValue(parsed);
|
|
161
|
+
if (verdict) {
|
|
162
|
+
return verdict;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
export function parseCliJudgeOutput(providerId, raw) {
|
|
168
|
+
const direct = parseTier1Json(raw);
|
|
169
|
+
if (direct) {
|
|
170
|
+
return direct;
|
|
171
|
+
}
|
|
172
|
+
const parsed = tryParseJson(raw);
|
|
173
|
+
if (parsed) {
|
|
174
|
+
const envelopeVerdict = extractVerdictFromJsonValue(parsed);
|
|
175
|
+
if (envelopeVerdict) {
|
|
176
|
+
return envelopeVerdict;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (providerId === 'codex') {
|
|
180
|
+
return parseCliJsonLines(raw);
|
|
181
|
+
}
|
|
182
|
+
return parseCliJsonLines(raw);
|
|
183
|
+
}
|
|
184
|
+
async function runCliJson(invocation, timeoutMs) {
|
|
185
|
+
return runCliJsonWithTimeouts(invocation, { evalTimeoutMs: timeoutMs });
|
|
186
|
+
}
|
|
187
|
+
export async function runCliJsonWithTimeouts(invocation, budgets) {
|
|
188
|
+
const { binary, args, stdin } = invocation;
|
|
189
|
+
const connectTimeoutMs = budgets.connectTimeoutMs ?? 0;
|
|
190
|
+
if (connectTimeoutMs > 0) {
|
|
191
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
192
|
+
}
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
const child = spawn(binary, args, {
|
|
195
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
196
|
+
});
|
|
197
|
+
const command = [binary, ...args].join(' ');
|
|
198
|
+
let stdout = '';
|
|
199
|
+
let stderr = '';
|
|
200
|
+
let settled = false;
|
|
201
|
+
let timer = null;
|
|
202
|
+
let forceKillTimer = null;
|
|
203
|
+
const cleanup = () => {
|
|
204
|
+
if (timer) {
|
|
205
|
+
clearTimeout(timer);
|
|
206
|
+
}
|
|
207
|
+
if (forceKillTimer) {
|
|
208
|
+
clearTimeout(forceKillTimer);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
const rejectOnce = (error) => {
|
|
212
|
+
if (settled)
|
|
213
|
+
return;
|
|
214
|
+
settled = true;
|
|
215
|
+
cleanup();
|
|
216
|
+
reject(error);
|
|
217
|
+
};
|
|
218
|
+
const resolveOnce = (value) => {
|
|
219
|
+
if (settled)
|
|
220
|
+
return;
|
|
221
|
+
settled = true;
|
|
222
|
+
cleanup();
|
|
223
|
+
resolve(value);
|
|
224
|
+
};
|
|
225
|
+
timer = setTimeout(() => {
|
|
226
|
+
rejectOnce(new CliRunError('timeout', `${binary} timed out`, {
|
|
227
|
+
stderr: stderr.trim() || undefined,
|
|
228
|
+
stdout: stdout.trim() || undefined,
|
|
229
|
+
command,
|
|
230
|
+
}));
|
|
231
|
+
try {
|
|
232
|
+
child.kill('SIGTERM');
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
forceKillTimer = setTimeout(() => {
|
|
238
|
+
try {
|
|
239
|
+
child.kill('SIGKILL');
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// process already exited
|
|
243
|
+
}
|
|
244
|
+
}, FORCE_KILL_GRACE_MS);
|
|
245
|
+
}, budgets.evalTimeoutMs);
|
|
246
|
+
child.stdout.on('data', (chunk) => {
|
|
247
|
+
stdout += String(chunk);
|
|
248
|
+
});
|
|
249
|
+
child.stderr.on('data', (chunk) => {
|
|
250
|
+
stderr += String(chunk);
|
|
251
|
+
});
|
|
252
|
+
if (stdin !== undefined) {
|
|
253
|
+
child.stdin.write(stdin);
|
|
254
|
+
}
|
|
255
|
+
child.stdin.end();
|
|
256
|
+
child.on('error', (error) => {
|
|
257
|
+
rejectOnce(new CliRunError('spawn_error', error instanceof Error ? error.message : String(error), {
|
|
258
|
+
command,
|
|
259
|
+
}));
|
|
260
|
+
});
|
|
261
|
+
child.on('close', (code) => {
|
|
262
|
+
if (settled)
|
|
263
|
+
return;
|
|
264
|
+
if (code === 0 && stdout.trim()) {
|
|
265
|
+
resolveOnce(stdout.trim());
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
rejectOnce(new CliRunError('exit_nonzero', stderr.trim() || `${binary} exited with code ${code ?? 'unknown'}`, {
|
|
269
|
+
exitCode: typeof code === 'number' ? code : undefined,
|
|
270
|
+
stderr: stderr.trim() || undefined,
|
|
271
|
+
stdout: stdout.trim() || undefined,
|
|
272
|
+
command,
|
|
273
|
+
}));
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
function createCliJudge(options) {
|
|
278
|
+
const transport = transportForProvider(options.providerId);
|
|
279
|
+
const timeoutMs = options.timeoutMs ?? 25000;
|
|
280
|
+
const providerId = options.providerId;
|
|
281
|
+
const runCommand = options.runCliCommand ?? runCliJson;
|
|
282
|
+
const runtime = options.runtime ?? DEFAULT_JUDGE_RUNTIME_CONFIG;
|
|
283
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
284
|
+
const stateDir = options.stateDir ?? repoRoot;
|
|
285
|
+
const judgeMode = options.judgeMode ?? 'enforce';
|
|
286
|
+
const judge = {
|
|
287
|
+
lastTrace: initialTrace(options),
|
|
288
|
+
async evaluate(input) {
|
|
289
|
+
const started = Date.now();
|
|
290
|
+
const prescan = input.innerCode ? prescanInterpreterCode(input.innerCode) : null;
|
|
291
|
+
if (prescan?.destroys_history_or_secrets) {
|
|
292
|
+
judge.lastTrace = {
|
|
293
|
+
provider: traceProvider(options.providerId),
|
|
294
|
+
modelRequested: options.modelRequested,
|
|
295
|
+
modelResolved: options.modelResolved,
|
|
296
|
+
latencyMs: Date.now() - started,
|
|
297
|
+
transport,
|
|
298
|
+
};
|
|
299
|
+
return prescan;
|
|
300
|
+
}
|
|
301
|
+
const scrubbed = scrubOutboundForJudge(input.text, {
|
|
302
|
+
sensitivePaths: options.sensitivePaths,
|
|
303
|
+
scrubOptions: options.scrubOptions,
|
|
304
|
+
});
|
|
305
|
+
if (!scrubbed.ok) {
|
|
306
|
+
judge.lastTrace = {
|
|
307
|
+
provider: 'fallback',
|
|
308
|
+
modelRequested: options.modelRequested,
|
|
309
|
+
modelResolved: options.modelResolved,
|
|
310
|
+
latencyMs: Date.now() - started,
|
|
311
|
+
fallbackReason: scrubbed.reason,
|
|
312
|
+
};
|
|
313
|
+
return failClosedVerdict('outbound_scrub_failed');
|
|
314
|
+
}
|
|
315
|
+
const prompt = buildTier1Prompt(scrubbed.text);
|
|
316
|
+
try {
|
|
317
|
+
const transportResult = await evaluateWithJudgeTransport({
|
|
318
|
+
prompt,
|
|
319
|
+
context: {
|
|
320
|
+
providerId,
|
|
321
|
+
model: options.modelResolved,
|
|
322
|
+
repoRoot: input.context.repoRoot || repoRoot,
|
|
323
|
+
stateDir,
|
|
324
|
+
judgeMode,
|
|
325
|
+
runtime,
|
|
326
|
+
judgeTimeoutMs: timeoutMs,
|
|
327
|
+
},
|
|
328
|
+
}, { runCommand });
|
|
329
|
+
const traceTransport = transportResult.transport === 'session'
|
|
330
|
+
? `${providerId}-cli-session`
|
|
331
|
+
: transport;
|
|
332
|
+
if (!transportResult.verdict) {
|
|
333
|
+
const fallbackReason = transportResult.fallbackReason ??
|
|
334
|
+
`${providerId}_cli_parse_error`;
|
|
335
|
+
judge.lastTrace = {
|
|
336
|
+
provider: 'fallback',
|
|
337
|
+
modelRequested: options.modelRequested,
|
|
338
|
+
modelResolved: options.modelResolved,
|
|
339
|
+
latencyMs: Date.now() - started,
|
|
340
|
+
fallbackReason,
|
|
341
|
+
judgeFallbackReason: fallbackReason,
|
|
342
|
+
judgeSessionUsed: transportResult.sessionUsed,
|
|
343
|
+
judgeSessionReused: transportResult.sessionReused,
|
|
344
|
+
judgeSessionRefHash: transportResult.sessionRefHash,
|
|
345
|
+
judgeSessionResetReason: transportResult.sessionResetReason,
|
|
346
|
+
judgeConnectMs: transportResult.connectMs,
|
|
347
|
+
judgeEvalMs: transportResult.evalMs,
|
|
348
|
+
judgeParseMs: transportResult.parseMs,
|
|
349
|
+
judgeShadowCompared: transportResult.shadowCompared,
|
|
350
|
+
judgeShadowMismatch: transportResult.shadowMismatch,
|
|
351
|
+
judgeShadowMismatchRateWindow: transportResult.shadowMismatchRateWindow,
|
|
352
|
+
judgeKillSwitchTriggered: transportResult.killSwitchTriggered,
|
|
353
|
+
};
|
|
354
|
+
return failClosedVerdict(transportFallbackToFailClosedReason(providerId, fallbackReason));
|
|
355
|
+
}
|
|
356
|
+
judge.lastTrace = {
|
|
357
|
+
provider: traceProvider(options.providerId),
|
|
358
|
+
modelRequested: options.modelRequested,
|
|
359
|
+
modelResolved: options.modelResolved,
|
|
360
|
+
latencyMs: Date.now() - started,
|
|
361
|
+
transport: traceTransport,
|
|
362
|
+
outboundRedacted: true,
|
|
363
|
+
judgeSessionUsed: transportResult.sessionUsed,
|
|
364
|
+
judgeSessionReused: transportResult.sessionReused,
|
|
365
|
+
judgeSessionRefHash: transportResult.sessionRefHash,
|
|
366
|
+
judgeSessionResetReason: transportResult.sessionResetReason,
|
|
367
|
+
judgeFallbackReason: transportResult.fallbackReason,
|
|
368
|
+
judgeConnectMs: transportResult.connectMs,
|
|
369
|
+
judgeEvalMs: transportResult.evalMs,
|
|
370
|
+
judgeParseMs: transportResult.parseMs,
|
|
371
|
+
judgeShadowCompared: transportResult.shadowCompared,
|
|
372
|
+
judgeShadowMismatch: transportResult.shadowMismatch,
|
|
373
|
+
judgeShadowMismatchRateWindow: transportResult.shadowMismatchRateWindow,
|
|
374
|
+
judgeKillSwitchTriggered: transportResult.killSwitchTriggered,
|
|
375
|
+
};
|
|
376
|
+
return transportResult.verdict;
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
judge.lastTrace = {
|
|
380
|
+
provider: 'fallback',
|
|
381
|
+
modelRequested: options.modelRequested,
|
|
382
|
+
modelResolved: options.modelResolved,
|
|
383
|
+
latencyMs: Date.now() - started,
|
|
384
|
+
fallbackReason: `${providerId}_cli_unavailable`,
|
|
385
|
+
judgeFallbackReason: `${providerId}_cli_unavailable`,
|
|
386
|
+
};
|
|
387
|
+
return failClosedVerdict(`${providerId}_cli_unavailable`);
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
return judge;
|
|
392
|
+
}
|
|
393
|
+
export function createCodexCliJudge(options) {
|
|
394
|
+
return createCliJudge({ ...options, providerId: 'codex' });
|
|
395
|
+
}
|
|
396
|
+
export function createCursorCliJudge(options) {
|
|
397
|
+
return createCliJudge({ ...options, providerId: 'cursor' });
|
|
398
|
+
}
|
|
399
|
+
export function createClaudeCliJudge(options) {
|
|
400
|
+
return createCliJudge({ ...options, providerId: 'claude' });
|
|
401
|
+
}
|
|
@@ -9,17 +9,28 @@ export declare function loadPinnedJudgeModels(): Promise<{
|
|
|
9
9
|
ciPin: string;
|
|
10
10
|
};
|
|
11
11
|
}>;
|
|
12
|
-
export declare function
|
|
12
|
+
export declare function resolveJudgeModel(judge: BelayJudgeConfig): {
|
|
13
|
+
requested: string;
|
|
14
|
+
resolved: string;
|
|
15
|
+
};
|
|
16
|
+
/** @deprecated Use resolveJudgeModel */
|
|
17
|
+
export declare const resolveCloudModel: (requested: string, _pinned: {
|
|
18
|
+
autoResolved: string;
|
|
19
|
+
}) => {
|
|
20
|
+
requested: string;
|
|
21
|
+
resolved: string;
|
|
22
|
+
};
|
|
23
|
+
/** @deprecated Use resolveJudgeModel */
|
|
24
|
+
export declare const resolveCursorModel: (requested: string, _pinned: {
|
|
13
25
|
autoResolved: string;
|
|
14
|
-
})
|
|
26
|
+
}) => {
|
|
15
27
|
requested: string;
|
|
16
28
|
resolved: string;
|
|
17
29
|
};
|
|
18
|
-
/** @deprecated Use resolveCloudModel */
|
|
19
|
-
export declare const resolveCursorModel: typeof resolveCloudModel;
|
|
20
30
|
export declare function createJudgeFromConfig(config: BelayConfigV4, options?: {
|
|
21
31
|
pinnedModels?: {
|
|
22
32
|
autoResolved: string;
|
|
23
33
|
};
|
|
34
|
+
repoRoot?: string;
|
|
24
35
|
}): TracedTier1Judge;
|
|
25
36
|
export declare function judgeConfigSummary(judge: BelayJudgeConfig): string;
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import {
|
|
3
|
+
import { repoLocalStateDirFor } from '../../config-io.js';
|
|
4
|
+
import { belayStateDir, normalizeJudgeProvider, scrubOptionsFromConfig } from '../config.js';
|
|
5
|
+
import { resolveJudgeCredential } from '../judge-api-key.js';
|
|
6
|
+
import { hasValidCloudConsent, isCloudJudgeConfig } from '../judge-config.js';
|
|
7
|
+
import { rejectDeprecatedJudgeModelAuto } from '../judge-model-policy.js';
|
|
8
|
+
import { detectJudgeRuntimeCapabilities } from '../judge-runtime-detection.js';
|
|
4
9
|
import { createDeterministicJudgeStub, createFailClosedJudge, createOllamaJudge, createOpenAiCompatibleJudge, } from './judge.js';
|
|
10
|
+
import { getJudgeProviderSpec, inferProviderIdFromConfig, isRemovedProviderId, normalizeLegacyProviderId, } from './judge-catalog.js';
|
|
11
|
+
import { createClaudeCliJudge, createCodexCliJudge, createCursorCliJudge } from './judge-cli.js';
|
|
5
12
|
const FIXTURE_MODELS_URL = new URL('../../../fixtures/judge-models.json', import.meta.url);
|
|
6
13
|
let cachedPinnedModels = null;
|
|
7
14
|
export function resetPinnedJudgeModelsCache() {
|
|
@@ -24,33 +31,104 @@ export async function loadPinnedJudgeModels() {
|
|
|
24
31
|
return cachedPinnedModels;
|
|
25
32
|
}
|
|
26
33
|
}
|
|
27
|
-
export function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
export function resolveJudgeModel(judge) {
|
|
35
|
+
rejectDeprecatedJudgeModelAuto(judge.model);
|
|
36
|
+
const requested = judge.model;
|
|
37
|
+
const envOverride = testJudgeModelOverride();
|
|
38
|
+
return { requested, resolved: envOverride || requested };
|
|
39
|
+
}
|
|
40
|
+
function testJudgeModelOverride() {
|
|
41
|
+
if (!process.env.VITEST && !process.env.VITEST_WORKER_ID) {
|
|
42
|
+
return undefined;
|
|
34
43
|
}
|
|
35
|
-
return
|
|
44
|
+
return process.env.BELAY_JUDGE_MODEL_RESOLVED?.trim() || undefined;
|
|
36
45
|
}
|
|
37
|
-
/** @deprecated Use
|
|
46
|
+
/** @deprecated Use resolveJudgeModel */
|
|
47
|
+
export const resolveCloudModel = (requested, _pinned) => {
|
|
48
|
+
rejectDeprecatedJudgeModelAuto(requested);
|
|
49
|
+
const envResolved = testJudgeModelOverride();
|
|
50
|
+
return {
|
|
51
|
+
requested,
|
|
52
|
+
resolved: envResolved || requested,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
/** @deprecated Use resolveJudgeModel */
|
|
38
56
|
export const resolveCursorModel = resolveCloudModel;
|
|
57
|
+
function providerIdFromJudge(judge) {
|
|
58
|
+
if (judge.providerId) {
|
|
59
|
+
const normalized = normalizeLegacyProviderId(judge.providerId);
|
|
60
|
+
if (normalized) {
|
|
61
|
+
return normalized;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return inferProviderIdFromConfig(judge);
|
|
65
|
+
}
|
|
66
|
+
function createCliJudgeForProvider(providerId, judgeConfig, config, repoRoot) {
|
|
67
|
+
const resolvedRepoRoot = repoRoot ?? process.cwd();
|
|
68
|
+
const { resolved } = resolveJudgeModel(judgeConfig);
|
|
69
|
+
const options = {
|
|
70
|
+
modelRequested: judgeConfig.model,
|
|
71
|
+
modelResolved: resolved,
|
|
72
|
+
timeoutMs: judgeConfig.timeoutMs,
|
|
73
|
+
sensitivePaths: config.classifier.sensitivePaths,
|
|
74
|
+
scrubOptions: scrubOptionsFromConfig(config),
|
|
75
|
+
runtime: judgeConfig.runtime,
|
|
76
|
+
repoRoot: resolvedRepoRoot,
|
|
77
|
+
stateDir: belayStateDir(config, repoLocalStateDirFor(resolvedRepoRoot, config)),
|
|
78
|
+
judgeMode: config.mode,
|
|
79
|
+
};
|
|
80
|
+
if (providerId === 'codex') {
|
|
81
|
+
return createCodexCliJudge(options);
|
|
82
|
+
}
|
|
83
|
+
if (providerId === 'cursor') {
|
|
84
|
+
return createCursorCliJudge(options);
|
|
85
|
+
}
|
|
86
|
+
return createClaudeCliJudge(options);
|
|
87
|
+
}
|
|
39
88
|
export function createJudgeFromConfig(config, options = {}) {
|
|
89
|
+
if (process.env.BELAY_DETERMINISTIC_JUDGE === '1') {
|
|
90
|
+
return createDeterministicJudgeStub();
|
|
91
|
+
}
|
|
40
92
|
const judgeConfig = config.judge;
|
|
93
|
+
if (judgeConfig.providerId && isRemovedProviderId(String(judgeConfig.providerId))) {
|
|
94
|
+
const { resolved } = resolveJudgeModel(judgeConfig);
|
|
95
|
+
return createFailClosedJudge({
|
|
96
|
+
reason: 'judge_provider_removed',
|
|
97
|
+
fallbackReason: 'provider_migration_required',
|
|
98
|
+
modelRequested: judgeConfig.model,
|
|
99
|
+
modelResolved: resolved,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
41
102
|
const provider = normalizeJudgeProvider(judgeConfig.provider);
|
|
103
|
+
const providerId = providerIdFromJudge(judgeConfig);
|
|
104
|
+
const catalogSpec = getJudgeProviderSpec(providerId);
|
|
105
|
+
const { resolved } = resolveJudgeModel(judgeConfig);
|
|
106
|
+
const runtime = detectJudgeRuntimeCapabilities(providerId);
|
|
107
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
42
108
|
if (provider === 'openai-compatible') {
|
|
43
109
|
const endpoint = judgeConfig.endpoint?.trim();
|
|
110
|
+
if (!endpoint && runtime.cliTransport) {
|
|
111
|
+
if (providerId === 'codex' || providerId === 'cursor') {
|
|
112
|
+
return createCliJudgeForProvider(providerId, judgeConfig, config, repoRoot);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
44
115
|
if (!endpoint) {
|
|
45
116
|
return createFailClosedJudge({
|
|
46
117
|
reason: 'openai_compatible_endpoint_missing',
|
|
47
118
|
fallbackReason: 'missing_endpoint',
|
|
48
119
|
modelRequested: judgeConfig.model,
|
|
49
|
-
modelResolved:
|
|
120
|
+
modelResolved: resolved,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (isCloudJudgeConfig(judgeConfig) && !hasValidCloudConsent(judgeConfig)) {
|
|
124
|
+
return createFailClosedJudge({
|
|
125
|
+
reason: 'openai_compatible_consent_missing',
|
|
126
|
+
fallbackReason: 'cloud_consent_missing',
|
|
127
|
+
modelRequested: judgeConfig.model,
|
|
128
|
+
modelResolved: resolved,
|
|
50
129
|
});
|
|
51
130
|
}
|
|
52
|
-
const
|
|
53
|
-
const { resolved } = resolveCloudModel(judgeConfig.model, pinned);
|
|
131
|
+
const repoLocalStateDir = repoLocalStateDirFor(repoRoot, config);
|
|
54
132
|
return createOpenAiCompatibleJudge({
|
|
55
133
|
endpoint,
|
|
56
134
|
modelRequested: judgeConfig.model,
|
|
@@ -58,6 +136,13 @@ export function createJudgeFromConfig(config, options = {}) {
|
|
|
58
136
|
timeoutMs: judgeConfig.timeoutMs,
|
|
59
137
|
sensitivePaths: config.classifier.sensitivePaths,
|
|
60
138
|
scrubOptions: scrubOptionsFromConfig(config),
|
|
139
|
+
resolveApiKey: async () => resolveJudgeCredential({
|
|
140
|
+
judge: judgeConfig,
|
|
141
|
+
catalogSpec: catalogSpec ?? undefined,
|
|
142
|
+
repoRoot,
|
|
143
|
+
repoLocalStateDir,
|
|
144
|
+
config,
|
|
145
|
+
}),
|
|
61
146
|
});
|
|
62
147
|
}
|
|
63
148
|
if (provider === 'ollama') {
|
|
@@ -68,8 +153,35 @@ export function createJudgeFromConfig(config, options = {}) {
|
|
|
68
153
|
keepAlive: judgeConfig.keepAlive,
|
|
69
154
|
});
|
|
70
155
|
}
|
|
71
|
-
|
|
156
|
+
if (provider === 'anthropic') {
|
|
157
|
+
if (runtime.cliTransport) {
|
|
158
|
+
return createClaudeCliJudge({
|
|
159
|
+
modelRequested: judgeConfig.model,
|
|
160
|
+
modelResolved: resolved,
|
|
161
|
+
timeoutMs: judgeConfig.timeoutMs,
|
|
162
|
+
sensitivePaths: config.classifier.sensitivePaths,
|
|
163
|
+
scrubOptions: scrubOptionsFromConfig(config),
|
|
164
|
+
runtime: judgeConfig.runtime,
|
|
165
|
+
repoRoot,
|
|
166
|
+
stateDir: belayStateDir(config, repoLocalStateDirFor(repoRoot, config)),
|
|
167
|
+
judgeMode: config.mode,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return createFailClosedJudge({
|
|
171
|
+
reason: 'anthropic_not_implemented',
|
|
172
|
+
fallbackReason: 'anthropic_runtime_unavailable',
|
|
173
|
+
modelRequested: judgeConfig.model,
|
|
174
|
+
modelResolved: resolved,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return createFailClosedJudge({
|
|
178
|
+
reason: 'unsupported_judge_provider',
|
|
179
|
+
fallbackReason: 'unsupported_provider',
|
|
180
|
+
modelRequested: judgeConfig.model,
|
|
181
|
+
modelResolved: resolved,
|
|
182
|
+
});
|
|
72
183
|
}
|
|
73
184
|
export function judgeConfigSummary(judge) {
|
|
74
|
-
|
|
185
|
+
const id = judge.providerId ?? judge.provider;
|
|
186
|
+
return `${id}/${judge.model}`;
|
|
75
187
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { JudgeProviderId } from './judge-catalog.js';
|
|
2
|
+
export type JudgeSessionFailureMode = 'parse_error' | 'non_json_response' | 'timeout' | 'cli_nonzero_exit' | 'io_error' | 'version_mismatch' | 'unsupported_resume' | 'unsafe_option_rejected';
|
|
3
|
+
export interface JudgeProviderSessionCapability {
|
|
4
|
+
providerId: Exclude<JudgeProviderId, 'ollama'>;
|
|
5
|
+
supportsSession: boolean;
|
|
6
|
+
resumeFlag: string | null;
|
|
7
|
+
createChatFlag: string | null;
|
|
8
|
+
readOnlyArgs: string[];
|
|
9
|
+
rejectedUnsafeOptions: string[];
|
|
10
|
+
failureModes: JudgeSessionFailureMode[];
|
|
11
|
+
notes: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const JUDGE_PROVIDER_SESSION_MATRIX: Record<Exclude<JudgeProviderId, 'ollama'>, JudgeProviderSessionCapability>;
|
|
14
|
+
export declare function providerSupportsSession(providerId: Exclude<JudgeProviderId, 'ollama'>, allowlist: JudgeProviderId[]): boolean;
|
|
15
|
+
export declare function assertReadOnlyInvocationArgs(providerId: Exclude<JudgeProviderId, 'ollama'>, args: string[]): {
|
|
16
|
+
ok: true;
|
|
17
|
+
} | {
|
|
18
|
+
ok: false;
|
|
19
|
+
reason: JudgeSessionFailureMode;
|
|
20
|
+
};
|