@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.
Files changed (180) hide show
  1. package/README.md +39 -12
  2. package/dist/adapters/cursor/runtime-entry.js +1 -0
  3. package/dist/adapters/shared/gate-runtime.d.ts +11 -14
  4. package/dist/adapters/shared/gate-runtime.js +289 -23
  5. package/dist/bundle/claude-runtime.mjs +4435 -708
  6. package/dist/bundle/codex-runtime.mjs +4448 -710
  7. package/dist/bundle/cursor-runtime.mjs +4437 -710
  8. package/dist/cli.js +333 -11
  9. package/dist/commands/approve.d.ts +1 -0
  10. package/dist/commands/approve.js +56 -26
  11. package/dist/commands/config.d.ts +70 -0
  12. package/dist/commands/config.js +577 -0
  13. package/dist/commands/doctor.js +13 -2
  14. package/dist/commands/explain.js +6 -1
  15. package/dist/commands/harvest.d.ts +29 -0
  16. package/dist/commands/harvest.js +72 -0
  17. package/dist/commands/health-snapshot.d.ts +6 -0
  18. package/dist/commands/health-snapshot.js +34 -0
  19. package/dist/commands/judge.d.ts +100 -0
  20. package/dist/commands/judge.js +304 -0
  21. package/dist/commands/metrics.js +34 -2
  22. package/dist/commands/quality.d.ts +38 -0
  23. package/dist/commands/quality.js +105 -0
  24. package/dist/commands/simulate.d.ts +1 -0
  25. package/dist/commands/simulate.js +7 -2
  26. package/dist/commands/standing-allow.d.ts +10 -0
  27. package/dist/commands/standing-allow.js +26 -0
  28. package/dist/commands/status.js +2 -2
  29. package/dist/commands/stdin-key.d.ts +1 -0
  30. package/dist/commands/stdin-key.js +8 -0
  31. package/dist/commands/tui.d.ts +48 -0
  32. package/dist/commands/tui.js +150 -0
  33. package/dist/config-io.js +10 -5
  34. package/dist/conformance/guarantee-table.js +12 -0
  35. package/dist/conformance/types.d.ts +2 -0
  36. package/dist/core/approval-replay-cli.d.ts +7 -0
  37. package/dist/core/approval-replay-cli.js +36 -0
  38. package/dist/core/approval-replay.d.ts +45 -0
  39. package/dist/core/approval-replay.js +141 -0
  40. package/dist/core/approval-service.d.ts +15 -0
  41. package/dist/core/approval-service.js +47 -2
  42. package/dist/core/approval.d.ts +24 -3
  43. package/dist/core/approval.js +47 -4
  44. package/dist/core/audit-analysis.d.ts +7 -1
  45. package/dist/core/audit-analysis.js +111 -0
  46. package/dist/core/audit-io.d.ts +2 -0
  47. package/dist/core/audit-io.js +14 -0
  48. package/dist/core/audit-metrics.d.ts +7 -0
  49. package/dist/core/audit-metrics.js +24 -4
  50. package/dist/core/audit-query.d.ts +1 -0
  51. package/dist/core/audit-query.js +3 -0
  52. package/dist/core/audit-replay-context.d.ts +35 -0
  53. package/dist/core/audit-replay-context.js +88 -0
  54. package/dist/core/audit-types.d.ts +24 -1
  55. package/dist/core/audit-types.js +1 -1
  56. package/dist/core/capability/index.d.ts +4 -3
  57. package/dist/core/capability/index.js +3 -2
  58. package/dist/core/capability/paths.d.ts +2 -1
  59. package/dist/core/capability/paths.js +76 -8
  60. package/dist/core/capability/reasons.d.ts +3 -0
  61. package/dist/core/capability/reasons.js +8 -0
  62. package/dist/core/capability/trusted-workspace-roots.d.ts +25 -0
  63. package/dist/core/capability/trusted-workspace-roots.js +149 -0
  64. package/dist/core/capability/types.d.ts +11 -1
  65. package/dist/core/capability-approval.d.ts +2 -1
  66. package/dist/core/capability-approval.js +100 -2
  67. package/dist/core/classify-subagent.js +2 -20
  68. package/dist/core/classify-tool.js +129 -30
  69. package/dist/core/config-layers.js +2 -1
  70. package/dist/core/config.d.ts +40 -2
  71. package/dist/core/config.js +159 -11
  72. package/dist/core/credential-store.d.ts +11 -0
  73. package/dist/core/credential-store.js +60 -0
  74. package/dist/core/gate-engine.js +104 -13
  75. package/dist/core/harvest.d.ts +53 -0
  76. package/dist/core/harvest.js +276 -0
  77. package/dist/core/index.d.ts +9 -3
  78. package/dist/core/index.js +6 -2
  79. package/dist/core/integrity.d.ts +2 -0
  80. package/dist/core/integrity.js +13 -0
  81. package/dist/core/judge-api-key.d.ts +19 -0
  82. package/dist/core/judge-api-key.js +74 -0
  83. package/dist/core/judge-cloud-consent.d.ts +13 -0
  84. package/dist/core/judge-cloud-consent.js +38 -0
  85. package/dist/core/judge-config.d.ts +41 -4
  86. package/dist/core/judge-config.js +263 -57
  87. package/dist/core/judge-doctor.d.ts +9 -1
  88. package/dist/core/judge-doctor.js +199 -94
  89. package/dist/core/judge-model-discovery.d.ts +24 -0
  90. package/dist/core/judge-model-discovery.js +168 -0
  91. package/dist/core/judge-model-policy.d.ts +5 -0
  92. package/dist/core/judge-model-policy.js +21 -0
  93. package/dist/core/judge-runtime-detection.d.ts +9 -0
  94. package/dist/core/judge-runtime-detection.js +68 -0
  95. package/dist/core/path-utils.d.ts +11 -0
  96. package/dist/core/path-utils.js +56 -6
  97. package/dist/core/reclassify.d.ts +3 -0
  98. package/dist/core/reclassify.js +44 -14
  99. package/dist/core/replay-scrub.d.ts +9 -0
  100. package/dist/core/replay-scrub.js +43 -0
  101. package/dist/core/shell-tokenizer.d.ts +2 -0
  102. package/dist/core/shell-tokenizer.js +82 -23
  103. package/dist/core/standing-allow.d.ts +50 -0
  104. package/dist/core/standing-allow.js +175 -0
  105. package/dist/core/transactional/diff-evaluator.js +1 -19
  106. package/dist/core/types.d.ts +17 -0
  107. package/dist/core/verdict/adapter.d.ts +1 -0
  108. package/dist/core/verdict/adapter.js +17 -2
  109. package/dist/core/verdict/containment.d.ts +8 -2
  110. package/dist/core/verdict/containment.js +45 -10
  111. package/dist/core/verdict/judge-audit.d.ts +1 -0
  112. package/dist/core/verdict/judge-audit.js +32 -1
  113. package/dist/core/verdict/judge-baseline.d.ts +20 -0
  114. package/dist/core/verdict/judge-baseline.js +48 -0
  115. package/dist/core/verdict/judge-broker-service.d.ts +42 -0
  116. package/dist/core/verdict/judge-broker-service.js +279 -0
  117. package/dist/core/verdict/judge-catalog.d.ts +40 -0
  118. package/dist/core/verdict/judge-catalog.js +148 -0
  119. package/dist/core/verdict/judge-cli-fingerprint.d.ts +5 -0
  120. package/dist/core/verdict/judge-cli-fingerprint.js +61 -0
  121. package/dist/core/verdict/judge-cli.d.ts +46 -0
  122. package/dist/core/verdict/judge-cli.js +401 -0
  123. package/dist/core/verdict/judge-factory.d.ts +15 -4
  124. package/dist/core/verdict/judge-factory.js +127 -15
  125. package/dist/core/verdict/judge-provider-matrix.d.ts +20 -0
  126. package/dist/core/verdict/judge-provider-matrix.js +66 -0
  127. package/dist/core/verdict/judge-runtime-config.d.ts +42 -0
  128. package/dist/core/verdict/judge-runtime-config.js +92 -0
  129. package/dist/core/verdict/judge-session-broker.d.ts +48 -0
  130. package/dist/core/verdict/judge-session-broker.js +195 -0
  131. package/dist/core/verdict/judge-session-guard.d.ts +27 -0
  132. package/dist/core/verdict/judge-session-guard.js +91 -0
  133. package/dist/core/verdict/judge-session-kill-switch.d.ts +11 -0
  134. package/dist/core/verdict/judge-session-kill-switch.js +43 -0
  135. package/dist/core/verdict/judge-session-mutex.d.ts +9 -0
  136. package/dist/core/verdict/judge-session-mutex.js +23 -0
  137. package/dist/core/verdict/judge-shadow.d.ts +24 -0
  138. package/dist/core/verdict/judge-shadow.js +116 -0
  139. package/dist/core/verdict/judge-transport.d.ts +40 -0
  140. package/dist/core/verdict/judge-transport.js +258 -0
  141. package/dist/core/verdict/judge.d.ts +33 -1
  142. package/dist/core/verdict/judge.js +84 -14
  143. package/dist/core/verdict/parser.js +6 -1
  144. package/dist/core/verdict/persistent-paths.d.ts +8 -0
  145. package/dist/core/verdict/persistent-paths.js +52 -0
  146. package/dist/core/verdict/types.d.ts +19 -2
  147. package/dist/core/verdict/verdict.js +166 -49
  148. package/dist/corpus/adversarial-probe.d.ts +86 -0
  149. package/dist/corpus/adversarial-probe.js +220 -0
  150. package/dist/corpus/evaluate.d.ts +45 -12
  151. package/dist/corpus/evaluate.js +62 -5
  152. package/dist/corpus/gates.d.ts +35 -0
  153. package/dist/corpus/gates.js +81 -0
  154. package/dist/corpus/judge-accuracy.d.ts +8 -0
  155. package/dist/corpus/judge-accuracy.js +39 -0
  156. package/dist/corpus/must-allow-commands.d.ts +5 -0
  157. package/dist/corpus/must-allow-commands.js +13 -0
  158. package/dist/corpus/mutators.d.ts +28 -0
  159. package/dist/corpus/mutators.js +151 -0
  160. package/dist/corpus/ratchet.d.ts +42 -0
  161. package/dist/corpus/ratchet.js +116 -0
  162. package/dist/corpus/runtime-match.d.ts +19 -0
  163. package/dist/corpus/runtime-match.js +45 -0
  164. package/dist/corpus/standing-allow-catalog.generated.d.ts +13 -0
  165. package/dist/corpus/standing-allow-catalog.generated.js +79 -0
  166. package/dist/corpus/types.d.ts +47 -0
  167. package/dist/corpus/types.js +157 -0
  168. package/dist/installer.js +69 -15
  169. package/dist/judge-broker-daemon.d.ts +1 -0
  170. package/dist/judge-broker-daemon.js +123 -0
  171. package/dist/services/sandbox-service.d.ts +1 -0
  172. package/dist/services/sandbox-service.js +2 -0
  173. package/dist/types.d.ts +7 -0
  174. package/dist/version.d.ts +1 -1
  175. package/dist/version.js +1 -1
  176. package/package.json +7 -3
  177. package/skills/belay/SKILL.md +15 -8
  178. package/skills/belay/belay-approve.md +17 -0
  179. package/dist/commands/init-wizard.d.ts +0 -21
  180. package/dist/commands/init-wizard.js +0 -63
@@ -0,0 +1,52 @@
1
+ /**
2
+ * M3 catalog: paths that survive reboot or change agent startup.
3
+ * @see docs/adr/tier0-retention-ledger.md
4
+ */
5
+ export const PERSISTENT_AGENT_PATH_MARKERS = [
6
+ '.ssh/authorized_keys',
7
+ '.zshrc',
8
+ '.bashrc',
9
+ '.bash_profile',
10
+ '.profile',
11
+ '.config/',
12
+ '/crontab',
13
+ 'LaunchAgents/',
14
+ '/Library/LaunchDaemons/',
15
+ ];
16
+ export const OUTSIDE_REPO_SECRET_CREDENTIAL_MARKERS = [
17
+ '.env',
18
+ '.env.*',
19
+ '*.pem',
20
+ '.ssh/id_*',
21
+ '.aws/',
22
+ '.npmrc',
23
+ '.git-credentials',
24
+ '.netrc',
25
+ '.kube/config',
26
+ '.docker/config.json',
27
+ '.gnupg/',
28
+ '.pypirc',
29
+ ];
30
+ export function isPersistentAgentPath(resolvedPath) {
31
+ const normalized = resolvedPath.replaceAll('\\', '/');
32
+ return (/\.ssh\/authorized_keys$/i.test(normalized) ||
33
+ /\/\.(zshrc|bashrc|bash_profile|profile)$/i.test(normalized) ||
34
+ /\/\.config\//i.test(normalized) ||
35
+ /\/crontab$/i.test(normalized) ||
36
+ /LaunchAgents\//i.test(normalized) ||
37
+ /\/Library\/LaunchDaemons\//i.test(normalized));
38
+ }
39
+ export function isOutsideRepoSecretCredentialPath(resolvedPath) {
40
+ const normalized = resolvedPath.replaceAll('\\', '/');
41
+ return (/\/\.env(?:\.[^/]+)?$/i.test(normalized) ||
42
+ /\/[^/]+\.pem$/i.test(normalized) ||
43
+ /\/\.ssh\/id_[^/]+$/i.test(normalized) ||
44
+ /\/\.aws\//i.test(normalized) ||
45
+ /\/\.npmrc$/i.test(normalized) ||
46
+ /\/\.git-credentials$/i.test(normalized) ||
47
+ /\/\.netrc$/i.test(normalized) ||
48
+ /\/\.kube\/config$/i.test(normalized) ||
49
+ /\/\.docker\/config\.json$/i.test(normalized) ||
50
+ /\/\.gnupg\//i.test(normalized) ||
51
+ /\/\.pypirc$/i.test(normalized));
52
+ }
@@ -16,10 +16,14 @@ export interface VerdictResult {
16
16
  judgeTrace?: JudgeTrace;
17
17
  }
18
18
  export interface Tier1Verdict {
19
- external_change: boolean;
20
- destroys_outside_repo: boolean;
19
+ /** true = undoable via git/fs snapshot or trivial local revert (ADR-002 local-recoverable). */
20
+ local_recoverable: boolean;
21
21
  destroys_history_or_secrets: boolean;
22
22
  reason: string;
23
+ /** @deprecated Legacy LLM field; parsed only for migration. */
24
+ external_change?: boolean;
25
+ /** @deprecated Unused in decision logic. */
26
+ destroys_outside_repo?: boolean;
23
27
  }
24
28
  export interface Tier1EvaluateInput {
25
29
  text: string;
@@ -39,12 +43,25 @@ export interface JudgeTrace {
39
43
  latencyMs: number;
40
44
  outboundRedacted?: boolean;
41
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;
42
58
  }
43
59
  export type VerdictMode = 'enforce' | 'audit';
44
60
  export interface VerdictContext {
45
61
  cwd: string;
46
62
  repoRoot: string;
47
63
  trustedCwd: boolean;
64
+ trustedWorkspaceRoots?: string[];
48
65
  sensitivePaths: string[];
49
66
  protectedArtifactRoots?: string[];
50
67
  customAllowCommands?: string[];
@@ -1,10 +1,9 @@
1
- import path from 'node:path';
2
- import { relativeWithinRepo } from '../path-utils.js';
3
- import { extractRedirectTargets, tokenizeShell } from '../shell-tokenizer.js';
4
- import { analyzePathTargets, cwdRelative, resolveTrustedPath } from './containment.js';
1
+ import { resolveMutationTarget } from '../path-utils.js';
2
+ import { extractRedirectTargets, isFdDuplication, isRedirectOperator, tokenizeShell, } from '../shell-tokenizer.js';
3
+ import { analyzePathTargets, cwdRelative, isDestructiveHighStakesMutation, resolveTrustedPath, touchesProtectedRoot, } from './containment.js';
5
4
  import { classifyEgressTool } from './egress-classify.js';
6
5
  import { verdictFingerprint } from './fingerprint.js';
7
- import { prescanInterpreterCode, tier1RequiresAsk } from './judge.js';
6
+ import { mutationPrescanRequiresAsk, prescanInterpreterCode, tier1RequiresAsk } from './judge.js';
8
7
  import { isRoutineLauncher, resolveLauncherRecipe } from './launcher-resolve.js';
9
8
  import { allowFromCustomOverride, askFromCustomExternal, customAllowMatch, customExternalMatch, } from './overrides.js';
10
9
  import { extractRecursiveScript, isBareInterpreter, isVariableIndirectHead, parseSegment, peelTransparentWrappers, redactCommand, segmentOpacity, splitTopLevelSegments, substitutionInners, } from './parser.js';
@@ -197,7 +196,7 @@ function extractPathArgs(tokens) {
197
196
  const args = [...redirects];
198
197
  for (let index = 1; index < tokens.length; index += 1) {
199
198
  const token = tokens[index];
200
- if (!token || token.startsWith('-') || token === '>' || token === '>>' || token === '<') {
199
+ if (!token || token.startsWith('-') || isRedirectOperator(token) || isFdDuplication(token)) {
201
200
  continue;
202
201
  }
203
202
  if (redirects.includes(token)) {
@@ -257,13 +256,45 @@ function tier0ExternalMatch(key, head, tokens) {
257
256
  }
258
257
  return false;
259
258
  }
259
+ function isBelayJudgeSelfCommand(tokens) {
260
+ return (tokens[0] === 'belay' &&
261
+ tokens[1] === 'judge' &&
262
+ (tokens[2] === 'use' ||
263
+ tokens[2] === 'status' ||
264
+ tokens[2] === 'list' ||
265
+ tokens[2] === 'test' ||
266
+ tokens[2] === 'consent'));
267
+ }
268
+ function isBelayConfigSelfCommand(tokens) {
269
+ if (tokens[0] !== 'belay' || tokens[1] !== 'config') {
270
+ return false;
271
+ }
272
+ const sub = tokens[2];
273
+ if (!sub) {
274
+ return true;
275
+ }
276
+ if (sub === 'list' || sub === 'get' || sub === 'judge') {
277
+ return true;
278
+ }
279
+ if (sub === 'credential') {
280
+ return true;
281
+ }
282
+ if (sub === 'set' || sub === 'unset') {
283
+ const pathKey = tokens[3] ?? '';
284
+ return pathKey.startsWith('judge.');
285
+ }
286
+ return false;
287
+ }
260
288
  function isBelaySelfCommand(tokens) {
261
289
  const head = tokens[0] ?? '';
262
290
  const subcommand = tokens[1] ?? '';
263
- return head === 'belay' && BELAY_SELF_COMMANDS.has(subcommand);
291
+ return (head === 'belay' &&
292
+ (BELAY_SELF_COMMANDS.has(subcommand) ||
293
+ isBelayJudgeSelfCommand(tokens) ||
294
+ isBelayConfigSelfCommand(tokens)));
264
295
  }
265
296
  function tier0HighStakesRm(tokens, context) {
266
- const head = tokens[0] ?? '';
297
+ const head = (tokens[0] ?? '').split('/').pop() ?? '';
267
298
  if (head !== 'rm') {
268
299
  return null;
269
300
  }
@@ -273,6 +304,7 @@ function tier0HighStakesRm(tokens, context) {
273
304
  cwd: context.cwd,
274
305
  repoRoot: context.repoRoot,
275
306
  trustedCwd: context.trustedCwd,
307
+ trustedWorkspaceRoots: context.trustedWorkspaceRoots,
276
308
  sensitivePaths: context.sensitivePaths,
277
309
  protectedArtifactRoots: context.protectedArtifactRoots,
278
310
  });
@@ -286,31 +318,40 @@ function tier0HighStakesRm(tokens, context) {
286
318
  signals: ['missing_trusted_cwd', ...analysis.signals],
287
319
  });
288
320
  }
289
- if (analysis.isHighStakes) {
290
- return askVerdict({
291
- location: analysis.location,
292
- opacity: 'transparent',
293
- effect: 'local_mutation',
294
- confidence: 'deterministic',
295
- reason: 'high_stakes_path',
296
- signals: ['high_stakes_path', ...analysis.signals],
297
- });
298
- }
299
321
  for (const target of targets) {
300
- if (target === '~' || target.startsWith('~/') || target.startsWith('/')) {
301
- const resolved = path.resolve(target === '~' || target.startsWith('~/') ? (process.env.HOME ?? '/') : context.cwd, target);
302
- const relative = relativeWithinRepo(context.repoRoot, resolved);
303
- if (relative === null) {
304
- return askVerdict({
305
- location: 'repo_outside',
306
- opacity: 'transparent',
307
- effect: 'local_mutation',
308
- confidence: 'deterministic',
309
- reason: 'repo_outside_mutation',
310
- signals: ['repo_outside_mutation'],
311
- });
322
+ const normalized = target.replace(/^['"]|['"]$/g, '');
323
+ if (normalized === '~' || normalized === '$HOME' || normalized.startsWith('~/')) {
324
+ return askVerdict({
325
+ location: 'repo_outside',
326
+ opacity: 'transparent',
327
+ effect: 'local_mutation',
328
+ confidence: 'deterministic',
329
+ reason: 'high_stakes_path',
330
+ signals: ['catastrophic_home_deletion', ...analysis.signals],
331
+ });
332
+ }
333
+ }
334
+ if (analysis.isHighStakes && analysis.signals.includes('high_stakes_path')) {
335
+ let destructiveHighStakes = false;
336
+ for (const target of targets) {
337
+ const resolved = resolveTrustedPath(target, context.cwd, context.trustedCwd) ??
338
+ resolveMutationTarget(target, context.cwd);
339
+ if (resolved &&
340
+ isDestructiveHighStakesMutation(head, resolved, context.repoRoot, context.sensitivePaths, context.protectedArtifactRoots, context.trustedWorkspaceRoots)) {
341
+ destructiveHighStakes = true;
342
+ break;
312
343
  }
313
344
  }
345
+ if (destructiveHighStakes) {
346
+ return askVerdict({
347
+ location: analysis.location,
348
+ opacity: 'transparent',
349
+ effect: 'local_mutation',
350
+ confidence: 'deterministic',
351
+ reason: 'high_stakes_path',
352
+ signals: ['high_stakes_path', ...analysis.signals],
353
+ });
354
+ }
314
355
  }
315
356
  return null;
316
357
  }
@@ -515,11 +556,15 @@ async function evaluateSegment(command, context, depth) {
515
556
  effect = 'local_mutation';
516
557
  }
517
558
  const pathArgs = extractPathArgs(peeled);
559
+ if (extractRedirectTargets(peeled).length > 0 && effect === 'read_only') {
560
+ effect = 'local_mutation';
561
+ }
518
562
  const pathAnalysis = analyzePathTargets({
519
563
  targets: pathArgs,
520
564
  cwd: context.cwd,
521
565
  repoRoot: context.repoRoot,
522
566
  trustedCwd: context.trustedCwd,
567
+ trustedWorkspaceRoots: context.trustedWorkspaceRoots,
523
568
  sensitivePaths: context.sensitivePaths,
524
569
  protectedArtifactRoots: context.protectedArtifactRoots,
525
570
  });
@@ -545,15 +590,45 @@ async function evaluateSegment(command, context, depth) {
545
590
  });
546
591
  }
547
592
  }
548
- if (pathAnalysis.isHighStakes) {
549
- return askVerdict({
550
- location: pathAnalysis.location,
551
- opacity: 'transparent',
552
- effect: 'local_mutation',
553
- confidence: 'deterministic',
554
- reason: 'high_stakes_path',
555
- signals: pathAnalysis.signals,
556
- });
593
+ if ((effect === 'local_mutation' || effect === 'unknown') &&
594
+ context.protectedArtifactRoots &&
595
+ context.protectedArtifactRoots.length > 0) {
596
+ for (const target of pathArgs) {
597
+ const resolved = resolveTrustedPath(target, context.cwd, context.trustedCwd) ??
598
+ resolveMutationTarget(target, context.cwd);
599
+ if (resolved && touchesProtectedRoot(resolved, context.protectedArtifactRoots)) {
600
+ return askVerdict({
601
+ location: pathAnalysis.location,
602
+ opacity: 'transparent',
603
+ effect: 'local_mutation',
604
+ confidence: 'deterministic',
605
+ reason: 'high_stakes_path',
606
+ signals: ['high_stakes_path', 'control_plane_path'],
607
+ });
608
+ }
609
+ }
610
+ }
611
+ if (effect === 'local_mutation' || effect === 'unknown') {
612
+ let destructiveHighStakes = false;
613
+ for (const target of pathArgs) {
614
+ const resolved = resolveTrustedPath(target, context.cwd, context.trustedCwd) ??
615
+ resolveMutationTarget(target, context.cwd);
616
+ if (resolved &&
617
+ isDestructiveHighStakesMutation(segment.head, resolved, context.repoRoot, context.sensitivePaths, context.protectedArtifactRoots, context.trustedWorkspaceRoots)) {
618
+ destructiveHighStakes = true;
619
+ break;
620
+ }
621
+ }
622
+ if (destructiveHighStakes) {
623
+ return askVerdict({
624
+ location: pathAnalysis.location,
625
+ opacity: 'transparent',
626
+ effect: 'local_mutation',
627
+ confidence: 'deterministic',
628
+ reason: 'high_stakes_path',
629
+ signals: pathAnalysis.signals,
630
+ });
631
+ }
557
632
  }
558
633
  if (segment.head === 'find' && isFindDangerous(peeled)) {
559
634
  return askVerdict({
@@ -565,15 +640,15 @@ async function evaluateSegment(command, context, depth) {
565
640
  signals: ['find_dangerous_action'],
566
641
  });
567
642
  }
568
- if (pathAnalysis.location === 'repo_outside' || pathAnalysis.location === 'mixed') {
569
- const outsideEffect = effect === 'read_only' ? 'read_only' : effect === 'unknown' ? 'local_mutation' : effect;
570
- return askVerdict({
643
+ if (effect === 'read_only' &&
644
+ (pathAnalysis.location === 'repo_outside' || pathAnalysis.location === 'mixed')) {
645
+ return allowVerdict({
571
646
  location: pathAnalysis.location,
572
- opacity: 'transparent',
573
- effect: outsideEffect,
647
+ opacity,
648
+ effect: 'read_only',
574
649
  confidence: 'deterministic',
575
- reason: 'repo_outside_mutation',
576
- signals: ['repo_outside_mutation', ...pathAnalysis.signals],
650
+ reason: 'read_only',
651
+ signals: ['read_only', ...pathAnalysis.signals],
577
652
  });
578
653
  }
579
654
  if (pathAnalysis.location === 'unknown' &&
@@ -588,7 +663,35 @@ async function evaluateSegment(command, context, depth) {
588
663
  signals: ['unknown_location_mutation'],
589
664
  });
590
665
  }
591
- const needsTier1 = effect === 'unknown' || TIER0_EXTERNAL_HEADS.has(segment.head) || egressClass === 'ambiguous';
666
+ const outsideMutation = pathAnalysis.location === 'repo_outside' || pathAnalysis.location === 'mixed';
667
+ const mutationPrescan = (effect === 'local_mutation' || effect === 'unknown') && pathArgs.length > 0
668
+ ? mutationPrescanRequiresAsk({
669
+ targets: pathArgs,
670
+ cwd: context.cwd,
671
+ repoRoot: context.repoRoot,
672
+ trustedCwd: context.trustedCwd,
673
+ trustedWorkspaceRoots: context.trustedWorkspaceRoots,
674
+ sensitivePaths: context.sensitivePaths,
675
+ })
676
+ : null;
677
+ if (mutationPrescan) {
678
+ return askVerdict({
679
+ location: pathAnalysis.location === 'unknown'
680
+ ? 'unknown'
681
+ : pathAnalysis.location === 'repo_outside' || pathAnalysis.location === 'mixed'
682
+ ? pathAnalysis.location
683
+ : 'repo_local',
684
+ opacity,
685
+ effect: 'local_mutation',
686
+ confidence: 'deterministic',
687
+ reason: 'tier1_catastrophic',
688
+ signals: ['tier1_catastrophic', mutationPrescan.reason],
689
+ });
690
+ }
691
+ const needsTier1 = effect === 'unknown' ||
692
+ TIER0_EXTERNAL_HEADS.has(segment.head) ||
693
+ egressClass === 'ambiguous' ||
694
+ (outsideMutation && effect !== 'read_only');
592
695
  let tier1Trace;
593
696
  if (needsTier1) {
594
697
  const tier1Text = recursiveScript ?? command;
@@ -600,9 +703,13 @@ async function evaluateSegment(command, context, depth) {
600
703
  tier1Trace = context.judge.lastTrace;
601
704
  if (tier1RequiresAsk(tier1)) {
602
705
  return askVerdict({
603
- location: pathAnalysis.location === 'unknown' ? 'unknown' : 'repo_local',
706
+ location: pathAnalysis.location === 'unknown'
707
+ ? 'unknown'
708
+ : pathAnalysis.location === 'repo_outside' || pathAnalysis.location === 'mixed'
709
+ ? pathAnalysis.location
710
+ : 'repo_local',
604
711
  opacity,
605
- effect: tier1.external_change ? 'remote_mutation' : effect,
712
+ effect: !tier1.local_recoverable ? 'remote_mutation' : effect,
606
713
  confidence: 'llm',
607
714
  reason: 'tier1_catastrophic',
608
715
  signals: ['tier1_catastrophic', tier1.reason],
@@ -610,6 +717,16 @@ async function evaluateSegment(command, context, depth) {
610
717
  });
611
718
  }
612
719
  }
720
+ if (outsideMutation && effect !== 'read_only') {
721
+ return withJudgeTrace(allowVerdict({
722
+ location: pathAnalysis.location,
723
+ opacity,
724
+ effect: 'local_mutation',
725
+ confidence: tier1Trace ? 'llm' : 'deterministic',
726
+ reason: 'repo_outside_local_mutation',
727
+ signals: ['repo_outside_local_mutation', ...pathAnalysis.signals],
728
+ }), tier1Trace);
729
+ }
613
730
  if (pathAnalysis.location === 'repo_local' &&
614
731
  (effect === 'read_only' || effect === 'local_mutation') &&
615
732
  opacity !== 'opaque') {
@@ -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 {};