@guilz-dev/belay 0.3.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/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 +277 -19
- package/dist/bundle/claude-runtime.mjs +3138 -699
- package/dist/bundle/codex-runtime.mjs +3162 -711
- package/dist/bundle/cursor-runtime.mjs +3140 -701
- package/dist/cli.js +137 -5
- package/dist/commands/approve.d.ts +1 -0
- package/dist/commands/approve.js +45 -26
- package/dist/commands/config.js +135 -52
- package/dist/commands/doctor.js +11 -1
- 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/judge.d.ts +11 -1
- package/dist/commands/judge.js +21 -2
- 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/tui.d.ts +48 -0
- package/dist/commands/tui.js +150 -0
- package/dist/config-io.js +10 -5
- 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-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 +2 -1
- package/dist/core/capability/index.js +1 -0
- package/dist/core/capability/paths.d.ts +2 -2
- package/dist/core/capability/paths.js +9 -9
- 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 +32 -3
- package/dist/core/config.d.ts +18 -0
- package/dist/core/config.js +49 -0
- package/dist/core/gate-engine.js +6 -6
- 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/judge-doctor.d.ts +3 -0
- package/dist/core/judge-doctor.js +54 -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/types.d.ts +15 -0
- package/dist/core/verdict/adapter.js +10 -1
- package/dist/core/verdict/containment.d.ts +4 -3
- package/dist/core/verdict/containment.js +17 -12
- 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-cli-fingerprint.d.ts +5 -0
- package/dist/core/verdict/judge-cli-fingerprint.js +61 -0
- package/dist/core/verdict/judge-cli.d.ts +23 -0
- package/dist/core/verdict/judge-cli.js +137 -16
- package/dist/core/verdict/judge-factory.js +13 -4
- 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 +14 -1
- package/dist/core/verdict/judge.js +6 -5
- package/dist/core/verdict/parser.js +6 -1
- package/dist/core/verdict/types.d.ts +13 -0
- package/dist/core/verdict/verdict.js +7 -4
- 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 +4 -1
- 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/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -3
- package/skills/belay/SKILL.md +7 -1
- package/skills/belay/belay-approve.md +17 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { hashJudgeSessionRef } from './judge-audit.js';
|
|
2
|
+
import { recordJudgeLatency } from './judge-baseline.js';
|
|
3
|
+
import { evaluateViaJudgeBroker, invalidateJudgeSession } from './judge-broker-service.js';
|
|
4
|
+
import { buildCliInvocation, CliRunError, parseCliJudgeOutput, runCliJsonWithTimeouts, } from './judge-cli.js';
|
|
5
|
+
import { resolveCliVersionFingerprint } from './judge-cli-fingerprint.js';
|
|
6
|
+
import { assertReadOnlyInvocationArgs, providerSupportsSession } from './judge-provider-matrix.js';
|
|
7
|
+
import { resolveSessionEvalTimeoutMs } from './judge-runtime-config.js';
|
|
8
|
+
import { buildJudgeSessionKey, exceedsPromptBudget, guardFailClosedFallbackReason, } from './judge-session-guard.js';
|
|
9
|
+
import { isJudgeSessionKillSwitchActive, recordShadowComparison, shouldRunShadowComparison, triggerJudgeSessionKillSwitch, verdictsEquivalent, } from './judge-shadow.js';
|
|
10
|
+
function isEvalTimeoutError(error) {
|
|
11
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
12
|
+
return /timed out/i.test(message);
|
|
13
|
+
}
|
|
14
|
+
function classifyParseFailure(providerId, raw) {
|
|
15
|
+
const trimmed = raw.trim();
|
|
16
|
+
if (!trimmed) {
|
|
17
|
+
return 'non_json_response';
|
|
18
|
+
}
|
|
19
|
+
if (parseCliJudgeOutput(providerId, trimmed)) {
|
|
20
|
+
return 'parse_error';
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
JSON.parse(trimmed);
|
|
24
|
+
return 'parse_error';
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return 'non_json_response';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function spawnFallbackReason(providerId, error) {
|
|
31
|
+
if (error instanceof CliRunError) {
|
|
32
|
+
if (error.kind === 'timeout') {
|
|
33
|
+
return 'eval_timeout';
|
|
34
|
+
}
|
|
35
|
+
if (error.kind === 'exit_nonzero') {
|
|
36
|
+
return `${providerId}_cli_nonzero`;
|
|
37
|
+
}
|
|
38
|
+
if (error.kind === 'spawn_error') {
|
|
39
|
+
return `${providerId}_cli_spawn_error`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (isEvalTimeoutError(error)) {
|
|
43
|
+
return 'eval_timeout';
|
|
44
|
+
}
|
|
45
|
+
return `${providerId}_cli_unavailable`;
|
|
46
|
+
}
|
|
47
|
+
function parseWithBudget(providerId, raw) {
|
|
48
|
+
const started = Date.now();
|
|
49
|
+
const verdict = parseCliJudgeOutput(providerId, raw);
|
|
50
|
+
const parseMs = Date.now() - started;
|
|
51
|
+
recordJudgeLatency('tier1_parse', parseMs);
|
|
52
|
+
return { verdict, parseMs };
|
|
53
|
+
}
|
|
54
|
+
async function runSpawnTransport(request, runCommand) {
|
|
55
|
+
const { context, prompt } = request;
|
|
56
|
+
const sessionConfig = context.runtime.session;
|
|
57
|
+
const invocation = buildCliInvocation(context.providerId, prompt, context.model);
|
|
58
|
+
const readOnly = assertReadOnlyInvocationArgs(context.providerId, invocation.args);
|
|
59
|
+
if (!readOnly.ok) {
|
|
60
|
+
return {
|
|
61
|
+
raw: '',
|
|
62
|
+
verdict: null,
|
|
63
|
+
transport: 'spawn',
|
|
64
|
+
sessionUsed: false,
|
|
65
|
+
sessionReused: false,
|
|
66
|
+
fallbackReason: 'unsafe_option_rejected',
|
|
67
|
+
connectMs: 0,
|
|
68
|
+
evalMs: 0,
|
|
69
|
+
parseMs: 0,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const evalTimeoutMs = resolveSessionEvalTimeoutMs(sessionConfig, context.judgeTimeoutMs);
|
|
73
|
+
let raw = '';
|
|
74
|
+
let evalMs = 0;
|
|
75
|
+
const evalStarted = Date.now();
|
|
76
|
+
try {
|
|
77
|
+
raw = await runCommand(invocation, evalTimeoutMs);
|
|
78
|
+
evalMs = Date.now() - evalStarted;
|
|
79
|
+
recordJudgeLatency('tier1_spawn', evalMs);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
evalMs = Date.now() - evalStarted;
|
|
83
|
+
return {
|
|
84
|
+
raw: '',
|
|
85
|
+
verdict: null,
|
|
86
|
+
transport: 'spawn',
|
|
87
|
+
sessionUsed: false,
|
|
88
|
+
sessionReused: false,
|
|
89
|
+
fallbackReason: spawnFallbackReason(context.providerId, error),
|
|
90
|
+
connectMs: 0,
|
|
91
|
+
evalMs,
|
|
92
|
+
parseMs: 0,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const parsed = parseWithBudget(context.providerId, raw);
|
|
96
|
+
return {
|
|
97
|
+
raw,
|
|
98
|
+
verdict: parsed.verdict,
|
|
99
|
+
transport: 'spawn',
|
|
100
|
+
sessionUsed: false,
|
|
101
|
+
sessionReused: false,
|
|
102
|
+
fallbackReason: parsed.verdict ? undefined : classifyParseFailure(context.providerId, raw),
|
|
103
|
+
connectMs: 0,
|
|
104
|
+
evalMs,
|
|
105
|
+
parseMs: parsed.parseMs,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
async function runSessionTransport(request, runCommand) {
|
|
109
|
+
const { context, prompt } = request;
|
|
110
|
+
const sessionConfig = context.runtime.session;
|
|
111
|
+
const connectStarted = Date.now();
|
|
112
|
+
if (!sessionConfig.enabled ||
|
|
113
|
+
!providerSupportsSession(context.providerId, sessionConfig.providerAllowlist) ||
|
|
114
|
+
(await isJudgeSessionKillSwitchActive(context.repoRoot, context.stateDir))) {
|
|
115
|
+
return runSpawnTransport(request, runCommand);
|
|
116
|
+
}
|
|
117
|
+
let cliVersion = 'unknown';
|
|
118
|
+
try {
|
|
119
|
+
cliVersion = await resolveCliVersionFingerprint(context.providerId, sessionConfig.connectTimeoutMs);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return {
|
|
123
|
+
...(await runSpawnTransport(request, runCommand)),
|
|
124
|
+
fallbackReason: 'connect_timeout',
|
|
125
|
+
sessionUsed: false,
|
|
126
|
+
sessionReused: false,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const connectMs = Date.now() - connectStarted;
|
|
130
|
+
recordJudgeLatency('tier1_connect', connectMs);
|
|
131
|
+
const promptBytes = Buffer.byteLength(prompt, 'utf8');
|
|
132
|
+
if (exceedsPromptBudget(promptBytes, sessionConfig)) {
|
|
133
|
+
const spawnResult = await runSpawnTransport(request, runCommand);
|
|
134
|
+
return {
|
|
135
|
+
...spawnResult,
|
|
136
|
+
sessionUsed: false,
|
|
137
|
+
sessionReused: false,
|
|
138
|
+
sessionResetReason: 'max_prompt_bytes_exceeded',
|
|
139
|
+
fallbackReason: 'guard_reset',
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const keyParts = {
|
|
143
|
+
providerId: context.providerId,
|
|
144
|
+
model: context.model,
|
|
145
|
+
repoRoot: context.repoRoot,
|
|
146
|
+
judgeMode: context.judgeMode,
|
|
147
|
+
cliVersion,
|
|
148
|
+
};
|
|
149
|
+
const sessionKey = buildJudgeSessionKey(keyParts);
|
|
150
|
+
const sessionRefHash = hashJudgeSessionRef(sessionKey);
|
|
151
|
+
const invocation = buildCliInvocation(context.providerId, prompt, context.model);
|
|
152
|
+
const readOnly = assertReadOnlyInvocationArgs(context.providerId, invocation.args);
|
|
153
|
+
if (!readOnly.ok) {
|
|
154
|
+
return runSpawnTransport(request, runCommand);
|
|
155
|
+
}
|
|
156
|
+
const evalTimeoutMs = resolveSessionEvalTimeoutMs(sessionConfig, context.judgeTimeoutMs);
|
|
157
|
+
const evalStarted = Date.now();
|
|
158
|
+
let brokerResult;
|
|
159
|
+
try {
|
|
160
|
+
brokerResult = await evaluateViaJudgeBroker({
|
|
161
|
+
stateDir: context.stateDir,
|
|
162
|
+
repoRoot: context.repoRoot,
|
|
163
|
+
sessionConfig,
|
|
164
|
+
request: {
|
|
165
|
+
keyParts,
|
|
166
|
+
invocation,
|
|
167
|
+
promptBytes,
|
|
168
|
+
},
|
|
169
|
+
timeoutMs: evalTimeoutMs,
|
|
170
|
+
runCommand,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
const spawnResult = await runSpawnTransport(request, runCommand);
|
|
175
|
+
return {
|
|
176
|
+
...spawnResult,
|
|
177
|
+
transport: 'session',
|
|
178
|
+
sessionUsed: true,
|
|
179
|
+
sessionReused: false,
|
|
180
|
+
fallbackReason: spawnFallbackReason(context.providerId, error),
|
|
181
|
+
sessionResetReason: 'cli_error',
|
|
182
|
+
sessionRefHash,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const evalMs = Date.now() - evalStarted;
|
|
186
|
+
recordJudgeLatency('tier1_session', evalMs);
|
|
187
|
+
recordJudgeLatency('tier1_eval', evalMs);
|
|
188
|
+
const parsed = parseWithBudget(context.providerId, brokerResult.raw);
|
|
189
|
+
if (!parsed.verdict) {
|
|
190
|
+
const parseReason = classifyParseFailure(context.providerId, brokerResult.raw);
|
|
191
|
+
await invalidateJudgeSession({
|
|
192
|
+
stateDir: context.stateDir,
|
|
193
|
+
repoRoot: context.repoRoot,
|
|
194
|
+
sessionConfig,
|
|
195
|
+
sessionKey,
|
|
196
|
+
reason: parseReason === 'non_json_response' ? 'non_json_response' : 'parse_failure',
|
|
197
|
+
});
|
|
198
|
+
const spawnResult = await runSpawnTransport(request, runCommand);
|
|
199
|
+
return {
|
|
200
|
+
...spawnResult,
|
|
201
|
+
transport: 'session',
|
|
202
|
+
sessionUsed: true,
|
|
203
|
+
sessionReused: brokerResult.reused,
|
|
204
|
+
fallbackReason: guardFailClosedFallbackReason(parseReason === 'non_json_response' ? 'non_json_response' : 'parse_failure'),
|
|
205
|
+
sessionResetReason: parseReason === 'non_json_response' ? 'non_json_response' : 'parse_failure',
|
|
206
|
+
sessionRefHash,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
raw: brokerResult.raw,
|
|
211
|
+
verdict: parsed.verdict,
|
|
212
|
+
transport: 'session',
|
|
213
|
+
sessionUsed: true,
|
|
214
|
+
sessionReused: brokerResult.reused,
|
|
215
|
+
sessionResetReason: brokerResult.resetReason,
|
|
216
|
+
sessionRefHash,
|
|
217
|
+
connectMs,
|
|
218
|
+
evalMs,
|
|
219
|
+
parseMs: parsed.parseMs,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
export async function evaluateWithJudgeTransport(request, options = {}) {
|
|
223
|
+
const runCommand = options.runCommand ??
|
|
224
|
+
((invocation, timeoutMs) => runCliJsonWithTimeouts(invocation, { evalTimeoutMs: timeoutMs }));
|
|
225
|
+
const { context } = request;
|
|
226
|
+
const useSession = context.runtime.session.enabled &&
|
|
227
|
+
providerSupportsSession(context.providerId, context.runtime.session.providerAllowlist) &&
|
|
228
|
+
!(await isJudgeSessionKillSwitchActive(context.repoRoot, context.stateDir));
|
|
229
|
+
const primary = useSession
|
|
230
|
+
? await runSessionTransport(request, runCommand)
|
|
231
|
+
: await runSpawnTransport(request, runCommand);
|
|
232
|
+
if (!shouldRunShadowComparison(context.repoRoot, context.providerId, context.runtime.shadow)) {
|
|
233
|
+
return primary;
|
|
234
|
+
}
|
|
235
|
+
const shadowSpawn = primary.transport === 'session' ? await runSpawnTransport(request, runCommand) : primary;
|
|
236
|
+
if (!primary.verdict || !shadowSpawn.verdict) {
|
|
237
|
+
return primary;
|
|
238
|
+
}
|
|
239
|
+
const shadow = recordShadowComparison(context.repoRoot, context.runtime.shadow, !verdictsEquivalent(primary.verdict, shadowSpawn.verdict));
|
|
240
|
+
if (shadow.killSwitchTriggered) {
|
|
241
|
+
await triggerJudgeSessionKillSwitch(context.repoRoot, context.stateDir);
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
...primary,
|
|
245
|
+
shadowCompared: shadow.compared,
|
|
246
|
+
shadowMismatch: shadow.mismatch,
|
|
247
|
+
shadowMismatchRateWindow: shadow.mismatchRateWindow,
|
|
248
|
+
killSwitchTriggered: shadow.killSwitchTriggered,
|
|
249
|
+
verdict: shadow.mismatch ? shadowSpawn.verdict : primary.verdict,
|
|
250
|
+
fallbackReason: shadow.mismatch ? 'shadow_forced_spawn' : primary.fallbackReason,
|
|
251
|
+
transport: shadow.mismatch ? 'spawn' : primary.transport,
|
|
252
|
+
sessionUsed: shadow.mismatch ? false : primary.sessionUsed,
|
|
253
|
+
sessionReused: shadow.mismatch ? false : primary.sessionReused,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
export function buildTransportInvocation(providerId, prompt, model) {
|
|
257
|
+
return buildCliInvocation(providerId, prompt, model);
|
|
258
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ScrubOptions } from '../types.js';
|
|
2
2
|
import type { Tier1Judge, Tier1Verdict } from './types.js';
|
|
3
|
-
export type Tier1JudgeTransport = 'http' | 'ollama-http' | 'codex-cli' | 'cursor-cli' | 'claude-cli' | 'unavailable';
|
|
3
|
+
export type Tier1JudgeTransport = 'http' | 'ollama-http' | 'codex-cli' | 'cursor-cli' | 'claude-cli' | 'codex-cli-session' | 'cursor-cli-session' | 'claude-cli-session' | 'unavailable';
|
|
4
4
|
export interface Tier1JudgeTrace {
|
|
5
5
|
provider: 'openai-compatible' | 'ollama' | 'anthropic' | 'fallback';
|
|
6
6
|
modelRequested: string;
|
|
@@ -9,6 +9,18 @@ export interface Tier1JudgeTrace {
|
|
|
9
9
|
transport?: Tier1JudgeTransport;
|
|
10
10
|
outboundRedacted?: boolean;
|
|
11
11
|
fallbackReason?: string;
|
|
12
|
+
judgeSessionUsed?: boolean;
|
|
13
|
+
judgeSessionReused?: boolean;
|
|
14
|
+
judgeSessionRefHash?: string;
|
|
15
|
+
judgeSessionResetReason?: string;
|
|
16
|
+
judgeFallbackReason?: string;
|
|
17
|
+
judgeConnectMs?: number;
|
|
18
|
+
judgeEvalMs?: number;
|
|
19
|
+
judgeParseMs?: number;
|
|
20
|
+
judgeShadowCompared?: boolean;
|
|
21
|
+
judgeShadowMismatch?: boolean;
|
|
22
|
+
judgeShadowMismatchRateWindow?: number;
|
|
23
|
+
judgeKillSwitchTriggered?: boolean;
|
|
12
24
|
}
|
|
13
25
|
export interface TracedTier1Judge extends Tier1Judge {
|
|
14
26
|
lastTrace?: Tier1JudgeTrace;
|
|
@@ -21,6 +33,7 @@ export interface MutationPrescanParams {
|
|
|
21
33
|
cwd: string;
|
|
22
34
|
repoRoot: string;
|
|
23
35
|
trustedCwd: boolean;
|
|
36
|
+
trustedWorkspaceRoots?: string[];
|
|
24
37
|
sensitivePaths: string[];
|
|
25
38
|
}
|
|
26
39
|
/** ADR-002 M3: structural prescan for sensitive / persistent mutation targets (shell redirects, etc.). */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { matchesSensitivePath } from '../glob.js';
|
|
2
|
-
import {
|
|
2
|
+
import { resolveMutationTarget, resolveWorkspaceRootMatch } from '../path-utils.js';
|
|
3
3
|
import { resolveTrustedPath } from './containment.js';
|
|
4
4
|
import { scrubOutboundForJudge } from './judge-outbound.js';
|
|
5
5
|
import { isOutsideRepoSecretCredentialPath, isPersistentAgentPath } from './persistent-paths.js';
|
|
@@ -85,9 +85,9 @@ export function prescanMutationTargets(params) {
|
|
|
85
85
|
if (!resolved) {
|
|
86
86
|
continue;
|
|
87
87
|
}
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
90
|
-
matchesSensitivePath(
|
|
88
|
+
const workspaceMatch = resolveWorkspaceRootMatch(params.repoRoot, params.trustedWorkspaceRoots, resolved);
|
|
89
|
+
if (workspaceMatch !== null &&
|
|
90
|
+
matchesSensitivePath(workspaceMatch.relativePath.replaceAll('\\', '/'), params.sensitivePaths)) {
|
|
91
91
|
return {
|
|
92
92
|
local_recoverable: false,
|
|
93
93
|
destroys_outside_repo: false,
|
|
@@ -95,7 +95,8 @@ export function prescanMutationTargets(params) {
|
|
|
95
95
|
reason: 'sensitive_path_mutation',
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
|
-
if (
|
|
98
|
+
if ((workspaceMatch === null || workspaceMatch.kind === 'trusted') &&
|
|
99
|
+
isOutsideRepoSecretCredentialPath(resolved)) {
|
|
99
100
|
return {
|
|
100
101
|
local_recoverable: false,
|
|
101
102
|
destroys_outside_repo: false,
|
|
@@ -160,7 +160,12 @@ export function splitTopLevelSegments(command) {
|
|
|
160
160
|
}
|
|
161
161
|
};
|
|
162
162
|
for (const token of tokens) {
|
|
163
|
-
if (token === '&&' ||
|
|
163
|
+
if (token === '&&' ||
|
|
164
|
+
token === '||' ||
|
|
165
|
+
token === ';' ||
|
|
166
|
+
token === '|' ||
|
|
167
|
+
token === '&' ||
|
|
168
|
+
token === '|&') {
|
|
164
169
|
flush();
|
|
165
170
|
continue;
|
|
166
171
|
}
|
|
@@ -43,12 +43,25 @@ export interface JudgeTrace {
|
|
|
43
43
|
latencyMs: number;
|
|
44
44
|
outboundRedacted?: boolean;
|
|
45
45
|
fallbackReason?: string;
|
|
46
|
+
judgeSessionUsed?: boolean;
|
|
47
|
+
judgeSessionReused?: boolean;
|
|
48
|
+
judgeSessionRefHash?: string;
|
|
49
|
+
judgeSessionResetReason?: string;
|
|
50
|
+
judgeFallbackReason?: string;
|
|
51
|
+
judgeConnectMs?: number;
|
|
52
|
+
judgeEvalMs?: number;
|
|
53
|
+
judgeParseMs?: number;
|
|
54
|
+
judgeShadowCompared?: boolean;
|
|
55
|
+
judgeShadowMismatch?: boolean;
|
|
56
|
+
judgeShadowMismatchRateWindow?: number;
|
|
57
|
+
judgeKillSwitchTriggered?: boolean;
|
|
46
58
|
}
|
|
47
59
|
export type VerdictMode = 'enforce' | 'audit';
|
|
48
60
|
export interface VerdictContext {
|
|
49
61
|
cwd: string;
|
|
50
62
|
repoRoot: string;
|
|
51
63
|
trustedCwd: boolean;
|
|
64
|
+
trustedWorkspaceRoots?: string[];
|
|
52
65
|
sensitivePaths: string[];
|
|
53
66
|
protectedArtifactRoots?: string[];
|
|
54
67
|
customAllowCommands?: string[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveMutationTarget } from '../path-utils.js';
|
|
2
|
-
import { extractRedirectTargets, tokenizeShell } from '../shell-tokenizer.js';
|
|
2
|
+
import { extractRedirectTargets, isFdDuplication, isRedirectOperator, tokenizeShell, } from '../shell-tokenizer.js';
|
|
3
3
|
import { analyzePathTargets, cwdRelative, isDestructiveHighStakesMutation, resolveTrustedPath, touchesProtectedRoot, } from './containment.js';
|
|
4
4
|
import { classifyEgressTool } from './egress-classify.js';
|
|
5
5
|
import { verdictFingerprint } from './fingerprint.js';
|
|
@@ -196,7 +196,7 @@ function extractPathArgs(tokens) {
|
|
|
196
196
|
const args = [...redirects];
|
|
197
197
|
for (let index = 1; index < tokens.length; index += 1) {
|
|
198
198
|
const token = tokens[index];
|
|
199
|
-
if (!token || token.startsWith('-') || token
|
|
199
|
+
if (!token || token.startsWith('-') || isRedirectOperator(token) || isFdDuplication(token)) {
|
|
200
200
|
continue;
|
|
201
201
|
}
|
|
202
202
|
if (redirects.includes(token)) {
|
|
@@ -304,6 +304,7 @@ function tier0HighStakesRm(tokens, context) {
|
|
|
304
304
|
cwd: context.cwd,
|
|
305
305
|
repoRoot: context.repoRoot,
|
|
306
306
|
trustedCwd: context.trustedCwd,
|
|
307
|
+
trustedWorkspaceRoots: context.trustedWorkspaceRoots,
|
|
307
308
|
sensitivePaths: context.sensitivePaths,
|
|
308
309
|
protectedArtifactRoots: context.protectedArtifactRoots,
|
|
309
310
|
});
|
|
@@ -336,7 +337,7 @@ function tier0HighStakesRm(tokens, context) {
|
|
|
336
337
|
const resolved = resolveTrustedPath(target, context.cwd, context.trustedCwd) ??
|
|
337
338
|
resolveMutationTarget(target, context.cwd);
|
|
338
339
|
if (resolved &&
|
|
339
|
-
isDestructiveHighStakesMutation(head, resolved, context.repoRoot, context.sensitivePaths, context.protectedArtifactRoots)) {
|
|
340
|
+
isDestructiveHighStakesMutation(head, resolved, context.repoRoot, context.sensitivePaths, context.protectedArtifactRoots, context.trustedWorkspaceRoots)) {
|
|
340
341
|
destructiveHighStakes = true;
|
|
341
342
|
break;
|
|
342
343
|
}
|
|
@@ -563,6 +564,7 @@ async function evaluateSegment(command, context, depth) {
|
|
|
563
564
|
cwd: context.cwd,
|
|
564
565
|
repoRoot: context.repoRoot,
|
|
565
566
|
trustedCwd: context.trustedCwd,
|
|
567
|
+
trustedWorkspaceRoots: context.trustedWorkspaceRoots,
|
|
566
568
|
sensitivePaths: context.sensitivePaths,
|
|
567
569
|
protectedArtifactRoots: context.protectedArtifactRoots,
|
|
568
570
|
});
|
|
@@ -612,7 +614,7 @@ async function evaluateSegment(command, context, depth) {
|
|
|
612
614
|
const resolved = resolveTrustedPath(target, context.cwd, context.trustedCwd) ??
|
|
613
615
|
resolveMutationTarget(target, context.cwd);
|
|
614
616
|
if (resolved &&
|
|
615
|
-
isDestructiveHighStakesMutation(segment.head, resolved, context.repoRoot, context.sensitivePaths, context.protectedArtifactRoots)) {
|
|
617
|
+
isDestructiveHighStakesMutation(segment.head, resolved, context.repoRoot, context.sensitivePaths, context.protectedArtifactRoots, context.trustedWorkspaceRoots)) {
|
|
616
618
|
destructiveHighStakes = true;
|
|
617
619
|
break;
|
|
618
620
|
}
|
|
@@ -668,6 +670,7 @@ async function evaluateSegment(command, context, depth) {
|
|
|
668
670
|
cwd: context.cwd,
|
|
669
671
|
repoRoot: context.repoRoot,
|
|
670
672
|
trustedCwd: context.trustedCwd,
|
|
673
|
+
trustedWorkspaceRoots: context.trustedWorkspaceRoots,
|
|
671
674
|
sensitivePaths: context.sensitivePaths,
|
|
672
675
|
})
|
|
673
676
|
: null;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type MutatedCase } from './mutators.js';
|
|
2
|
+
declare const MUST_ASK_VERDICT: "deny_pending_approval";
|
|
3
|
+
export declare function probeCaseKey(entry: {
|
|
4
|
+
core: string;
|
|
5
|
+
mutatorId: string;
|
|
6
|
+
command: string;
|
|
7
|
+
}): string;
|
|
8
|
+
export interface ProbePassedCase {
|
|
9
|
+
core: string;
|
|
10
|
+
mutatorId: string;
|
|
11
|
+
command: string;
|
|
12
|
+
}
|
|
13
|
+
export interface AdversarialProbeFailure {
|
|
14
|
+
core: string;
|
|
15
|
+
mutatorId: string;
|
|
16
|
+
command: string;
|
|
17
|
+
expected: typeof MUST_ASK_VERDICT;
|
|
18
|
+
actual: string;
|
|
19
|
+
reason: string;
|
|
20
|
+
}
|
|
21
|
+
export interface AdversarialProbeFpFailure {
|
|
22
|
+
core: string;
|
|
23
|
+
mutatorId: string;
|
|
24
|
+
command: string;
|
|
25
|
+
actual: string;
|
|
26
|
+
reason: string;
|
|
27
|
+
}
|
|
28
|
+
export interface AdversarialProbeReport {
|
|
29
|
+
iteration: number;
|
|
30
|
+
generatedAt: string;
|
|
31
|
+
seed: number;
|
|
32
|
+
batchId: string;
|
|
33
|
+
sourceCommands: string[];
|
|
34
|
+
selectedMutators: string[];
|
|
35
|
+
maxCases?: number;
|
|
36
|
+
holdoutRatio: number;
|
|
37
|
+
totalCases: number;
|
|
38
|
+
fixSetSize: number;
|
|
39
|
+
holdoutSetSize: number;
|
|
40
|
+
fixSetFnRate: number;
|
|
41
|
+
firstPassFnRate: number;
|
|
42
|
+
/** null when no FP probe cases were evaluated. */
|
|
43
|
+
firstPassFpRate: number | null;
|
|
44
|
+
fpProbeSize: number;
|
|
45
|
+
/** null when holdout set is empty, or fix set FN rate is 0 but holdout FN > 0. */
|
|
46
|
+
holdoutFnRate: number | null;
|
|
47
|
+
/** holdoutFnRate / fixSetFnRate; 1.0 when both are 0. null when holdout empty. */
|
|
48
|
+
holdoutFixFnRateRatio: number | null;
|
|
49
|
+
passedCases: ProbePassedCase[];
|
|
50
|
+
failures: AdversarialProbeFailure[];
|
|
51
|
+
fpFailures: AdversarialProbeFpFailure[];
|
|
52
|
+
/** Populated by fix sessions / CI integration; empty when probe-only. */
|
|
53
|
+
filesChanged: string[];
|
|
54
|
+
}
|
|
55
|
+
export interface AdversarialProbeOptions {
|
|
56
|
+
seed?: number;
|
|
57
|
+
holdoutRatio?: number;
|
|
58
|
+
maxCases?: number;
|
|
59
|
+
outputDir?: string;
|
|
60
|
+
repoRoot?: string;
|
|
61
|
+
iteration?: number;
|
|
62
|
+
}
|
|
63
|
+
/** Mulberry32 PRNG for reproducible shuffles. */
|
|
64
|
+
export declare function createSeededRandom(seed: number): () => number;
|
|
65
|
+
export declare function shuffleWithSeed<T>(items: readonly T[], seed: number): T[];
|
|
66
|
+
export declare function splitFixHoldout<T>(items: readonly T[], seed: number, holdoutRatio?: number): {
|
|
67
|
+
fix: T[];
|
|
68
|
+
holdout: T[];
|
|
69
|
+
};
|
|
70
|
+
export declare function generateProbeCases(seed: number, maxCases?: number): MutatedCase[];
|
|
71
|
+
/** Benign must-allow cores × AUTO_LABEL mutators — FP over-stop probe (not for corpus auto-add). */
|
|
72
|
+
export declare function generateFpProbeCases(seed: number): MutatedCase[];
|
|
73
|
+
/** Ratio for overfitting detection; null when holdout unavailable or fix-perfect but holdout not. */
|
|
74
|
+
export declare function holdoutFixFnRateRatio(fixSetFnRate: number, holdoutFnRate: number | null): number | null;
|
|
75
|
+
export declare function runAdversarialProbe(options?: AdversarialProbeOptions): Promise<AdversarialProbeReport>;
|
|
76
|
+
export declare function defaultProbeOutputDir(): string;
|
|
77
|
+
export declare function formatProbeSummary(report: AdversarialProbeReport): string;
|
|
78
|
+
export declare function parseProbeCliArgs(argv: string[]): {
|
|
79
|
+
seed: number;
|
|
80
|
+
holdoutRatio: number;
|
|
81
|
+
maxCases?: number;
|
|
82
|
+
strict: boolean;
|
|
83
|
+
outputDir: string;
|
|
84
|
+
};
|
|
85
|
+
export declare function main(argv?: string[]): Promise<number>;
|
|
86
|
+
export {};
|