@guilz-dev/belay 0.1.1 → 0.3.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 (106) hide show
  1. package/README.md +55 -15
  2. package/dist/adapters/shared/gate-runtime.js +25 -9
  3. package/dist/bundle/claude-runtime.mjs +1786 -379
  4. package/dist/bundle/codex-runtime.mjs +1785 -379
  5. package/dist/bundle/cursor-runtime.mjs +1786 -379
  6. package/dist/cli.js +201 -11
  7. package/dist/commands/approve.js +11 -0
  8. package/dist/commands/classify-for-report.js +3 -3
  9. package/dist/commands/config.d.ts +70 -0
  10. package/dist/commands/config.js +494 -0
  11. package/dist/commands/doctor.js +3 -2
  12. package/dist/commands/explain.js +14 -14
  13. package/dist/commands/health-snapshot.d.ts +6 -0
  14. package/dist/commands/health-snapshot.js +34 -0
  15. package/dist/commands/judge.d.ts +90 -0
  16. package/dist/commands/judge.js +285 -0
  17. package/dist/commands/recover.js +2 -2
  18. package/dist/commands/status.js +2 -2
  19. package/dist/commands/stdin-key.d.ts +1 -0
  20. package/dist/commands/stdin-key.js +8 -0
  21. package/dist/conformance/guarantee-table.js +12 -0
  22. package/dist/conformance/types.d.ts +2 -0
  23. package/dist/core/approval.d.ts +3 -0
  24. package/dist/core/approval.js +18 -3
  25. package/dist/core/audit-io.d.ts +2 -0
  26. package/dist/core/audit-io.js +14 -0
  27. package/dist/core/audit-query.js +5 -1
  28. package/dist/core/capability/index.d.ts +2 -2
  29. package/dist/core/capability/index.js +2 -2
  30. package/dist/core/capability/paths.d.ts +1 -0
  31. package/dist/core/capability/paths.js +74 -6
  32. package/dist/core/capability/reasons.d.ts +3 -0
  33. package/dist/core/capability/reasons.js +8 -0
  34. package/dist/core/classify-tool.js +97 -27
  35. package/dist/core/config-layers.js +2 -1
  36. package/dist/core/config.d.ts +23 -3
  37. package/dist/core/config.js +112 -13
  38. package/dist/core/credential-store.d.ts +11 -0
  39. package/dist/core/credential-store.js +60 -0
  40. package/dist/core/gate-contract.d.ts +1 -1
  41. package/dist/core/gate-contract.js +1 -1
  42. package/dist/core/gate-engine.js +105 -14
  43. package/dist/core/index.d.ts +2 -2
  44. package/dist/core/index.js +2 -2
  45. package/dist/core/integrity.d.ts +2 -0
  46. package/dist/core/integrity.js +13 -0
  47. package/dist/core/judge-api-key.d.ts +19 -0
  48. package/dist/core/judge-api-key.js +74 -0
  49. package/dist/core/judge-cloud-consent.d.ts +13 -0
  50. package/dist/core/judge-cloud-consent.js +38 -0
  51. package/dist/core/judge-config.d.ts +45 -4
  52. package/dist/core/judge-config.js +266 -44
  53. package/dist/core/judge-doctor.d.ts +6 -1
  54. package/dist/core/judge-doctor.js +148 -97
  55. package/dist/core/judge-model-discovery.d.ts +24 -0
  56. package/dist/core/judge-model-discovery.js +168 -0
  57. package/dist/core/judge-model-policy.d.ts +5 -0
  58. package/dist/core/judge-model-policy.js +21 -0
  59. package/dist/core/judge-runtime-detection.d.ts +9 -0
  60. package/dist/core/judge-runtime-detection.js +68 -0
  61. package/dist/core/transactional/diff-evaluator.js +1 -19
  62. package/dist/core/types.d.ts +7 -3
  63. package/dist/core/{v2 → verdict}/adapter.d.ts +1 -0
  64. package/dist/core/{v2 → verdict}/adapter.js +16 -4
  65. package/dist/core/{v2 → verdict}/containment.d.ts +5 -0
  66. package/dist/core/{v2 → verdict}/containment.js +32 -2
  67. package/dist/core/{v2 → verdict}/egress-classify.js +3 -0
  68. package/dist/core/verdict/judge-catalog.d.ts +40 -0
  69. package/dist/core/verdict/judge-catalog.js +148 -0
  70. package/dist/core/verdict/judge-cli.d.ts +23 -0
  71. package/dist/core/verdict/judge-cli.js +280 -0
  72. package/dist/core/{v2 → verdict}/judge-factory.d.ts +15 -4
  73. package/dist/core/verdict/judge-factory.js +178 -0
  74. package/dist/core/{v2 → verdict}/judge.d.ts +20 -1
  75. package/dist/core/{v2 → verdict}/judge.js +85 -18
  76. package/dist/core/{v2 → verdict}/launcher-resolve.js +72 -1
  77. package/dist/core/verdict/persistent-paths.d.ts +8 -0
  78. package/dist/core/verdict/persistent-paths.js +52 -0
  79. package/dist/core/{v2 → verdict}/types.d.ts +6 -2
  80. package/dist/core/{v2 → verdict}/verdict.js +176 -46
  81. package/dist/corpus/evaluate.js +2 -2
  82. package/dist/installer.js +66 -14
  83. package/dist/types.d.ts +8 -1
  84. package/dist/version.d.ts +1 -1
  85. package/dist/version.js +1 -1
  86. package/package.json +2 -1
  87. package/skills/belay/SKILL.md +8 -7
  88. package/dist/commands/init-wizard.d.ts +0 -16
  89. package/dist/commands/init-wizard.js +0 -50
  90. package/dist/core/v2/judge-factory.js +0 -75
  91. /package/dist/core/{v2 → verdict}/egress-classify.d.ts +0 -0
  92. /package/dist/core/{v2 → verdict}/fingerprint.d.ts +0 -0
  93. /package/dist/core/{v2 → verdict}/fingerprint.js +0 -0
  94. /package/dist/core/{v2 → verdict}/index.d.ts +0 -0
  95. /package/dist/core/{v2 → verdict}/index.js +0 -0
  96. /package/dist/core/{v2 → verdict}/judge-audit.d.ts +0 -0
  97. /package/dist/core/{v2 → verdict}/judge-audit.js +0 -0
  98. /package/dist/core/{v2 → verdict}/judge-outbound.d.ts +0 -0
  99. /package/dist/core/{v2 → verdict}/judge-outbound.js +0 -0
  100. /package/dist/core/{v2 → verdict}/launcher-resolve.d.ts +0 -0
  101. /package/dist/core/{v2 → verdict}/overrides.d.ts +0 -0
  102. /package/dist/core/{v2 → verdict}/overrides.js +0 -0
  103. /package/dist/core/{v2 → verdict}/parser.d.ts +0 -0
  104. /package/dist/core/{v2 → verdict}/parser.js +0 -0
  105. /package/dist/core/{v2 → verdict}/types.js +0 -0
  106. /package/dist/core/{v2 → verdict}/verdict.d.ts +0 -0
@@ -1,35 +1,4 @@
1
1
  // agent-belay claude runtime bundle
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropNames = Object.getOwnPropertyNames;
4
- var __esm = (fn, res) => function __init() {
5
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
- };
7
- var __export = (target, all) => {
8
- for (var name in all)
9
- __defProp(target, name, { get: all[name], enumerable: true });
10
- };
11
-
12
- // src/core/judge-api-key.ts
13
- var judge_api_key_exports = {};
14
- __export(judge_api_key_exports, {
15
- resolveJudgeApiKey: () => resolveJudgeApiKey
16
- });
17
- function resolveJudgeApiKey(env = process.env) {
18
- const belay = env.BELAY_JUDGE_API_KEY?.trim();
19
- if (belay) {
20
- return { key: belay, source: "BELAY_JUDGE_API_KEY" };
21
- }
22
- const openai = env.OPENAI_API_KEY?.trim();
23
- if (openai) {
24
- return { key: openai, source: "OPENAI_API_KEY" };
25
- }
26
- return { key: null, source: null };
27
- }
28
- var init_judge_api_key = __esm({
29
- "src/core/judge-api-key.ts"() {
30
- "use strict";
31
- }
32
- });
33
2
 
34
3
  // src/adapters/claude/runtime-entry.ts
35
4
  import process2 from "node:process";
@@ -52,7 +21,7 @@ function classifyResultToGateVerdict(params) {
52
21
  approvalId,
53
22
  user_message,
54
23
  agent_message,
55
- v2: result.v2
24
+ axes: result.axes
56
25
  };
57
26
  }
58
27
  function unnormalizedGateVerdict(params) {
@@ -82,12 +51,158 @@ import path3 from "node:path";
82
51
  // src/core/config.ts
83
52
  import path from "node:path";
84
53
 
54
+ // src/core/judge-model-policy.ts
55
+ var DEPRECATED_JUDGE_MODEL_AUTO = "auto";
56
+ function isDeprecatedJudgeModelAuto(model) {
57
+ return model?.trim().toLowerCase() === DEPRECATED_JUDGE_MODEL_AUTO;
58
+ }
59
+ function rejectDeprecatedJudgeModelAuto(model) {
60
+ if (isDeprecatedJudgeModelAuto(model)) {
61
+ throw new Error(
62
+ 'judge model "auto" is no longer accepted. Use a concrete model id from belay judge list or the provider catalog default.'
63
+ );
64
+ }
65
+ }
66
+ var warnedDeprecatedAuto = false;
67
+ function warnDeprecatedJudgeModelAuto() {
68
+ if (warnedDeprecatedAuto) {
69
+ return;
70
+ }
71
+ warnedDeprecatedAuto = true;
72
+ process.stderr.write(
73
+ 'Warning: judge.model "auto" is deprecated; normalized to the provider catalog default on load. Set a concrete model id with belay config set judge.model <id>.\n'
74
+ );
75
+ }
76
+
77
+ // src/core/verdict/judge-catalog.ts
78
+ var LEGACY_PROVIDER_ID_ALIASES = {
79
+ local: "ollama",
80
+ openai: "codex"
81
+ };
82
+ var REMOVED_JUDGE_PROVIDER_IDS = ["openrouter", "custom"];
83
+ function isRemovedProviderId(id) {
84
+ return REMOVED_JUDGE_PROVIDER_IDS.includes(id);
85
+ }
86
+ function warnRemovedProviderId(id) {
87
+ process.stderr.write(
88
+ `Warning: judge.providerId "${id}" was removed; migrate with belay config set judge.providerId <ollama|codex|claude|cursor>.
89
+ `
90
+ );
91
+ }
92
+ var JUDGE_CATALOG = {
93
+ ollama: {
94
+ id: "ollama",
95
+ driver: "ollama",
96
+ defaultEndpoint: "http://localhost:11434",
97
+ defaultModel: "gemma4:e2b",
98
+ apiKeyEnvVars: [],
99
+ isCloud: false
100
+ },
101
+ codex: {
102
+ id: "codex",
103
+ driver: "openai-compatible",
104
+ defaultEndpoint: null,
105
+ defaultModel: "gpt-5.3-codex-high",
106
+ apiKeyEnvVars: ["OPENAI_API_KEY", "BELAY_JUDGE_API_KEY"],
107
+ isCloud: true
108
+ },
109
+ claude: {
110
+ id: "claude",
111
+ driver: "anthropic",
112
+ defaultEndpoint: null,
113
+ defaultModel: "claude-sonnet-4-6",
114
+ apiKeyEnvVars: ["ANTHROPIC_API_KEY", "BELAY_JUDGE_API_KEY"],
115
+ isCloud: true
116
+ },
117
+ cursor: {
118
+ id: "cursor",
119
+ driver: "openai-compatible",
120
+ defaultEndpoint: null,
121
+ defaultModel: "composer-2.5",
122
+ apiKeyEnvVars: ["CURSOR_API_KEY", "BELAY_JUDGE_API_KEY"],
123
+ isCloud: true
124
+ }
125
+ };
126
+ var JUDGE_PROVIDER_IDS = Object.keys(JUDGE_CATALOG);
127
+ function normalizeLegacyProviderId(id) {
128
+ if (id in JUDGE_CATALOG) {
129
+ return id;
130
+ }
131
+ return LEGACY_PROVIDER_ID_ALIASES[id] ?? null;
132
+ }
133
+ function getJudgeProviderSpec(id) {
134
+ const normalized = normalizeLegacyProviderId(id);
135
+ if (!normalized) {
136
+ return null;
137
+ }
138
+ return JUDGE_CATALOG[normalized];
139
+ }
140
+ function inferProviderIdFromConfig(judge) {
141
+ if (judge.providerId) {
142
+ const normalized = normalizeLegacyProviderId(judge.providerId);
143
+ if (normalized) {
144
+ return normalized;
145
+ }
146
+ }
147
+ if (judge.provider === "ollama") {
148
+ return "ollama";
149
+ }
150
+ if (judge.provider === "anthropic") {
151
+ return "claude";
152
+ }
153
+ const endpoint = judge.endpoint?.trim().toLowerCase() ?? "";
154
+ if (endpoint.includes("openrouter.ai")) {
155
+ return "codex";
156
+ }
157
+ if (endpoint.includes("api.openai.com")) {
158
+ return "codex";
159
+ }
160
+ if (endpoint) {
161
+ return "codex";
162
+ }
163
+ if (judge.provider === "openai-compatible") {
164
+ const model = judge.model?.trim().toLowerCase() ?? "";
165
+ if (model.includes("composer")) {
166
+ return "cursor";
167
+ }
168
+ }
169
+ return "codex";
170
+ }
171
+ function resolveJudgeFromCatalog(input) {
172
+ const providerId = normalizeLegacyProviderId(String(input.providerId));
173
+ if (!providerId) {
174
+ throw new Error(`Unknown judge provider id: ${input.providerId}`);
175
+ }
176
+ const spec = JUDGE_CATALOG[providerId];
177
+ const model = input.model?.trim() || spec.defaultModel;
178
+ const endpoint = input.endpoint !== void 0 ? input.endpoint?.trim() || null : spec.defaultEndpoint;
179
+ return {
180
+ provider: spec.driver,
181
+ providerId: spec.id,
182
+ model,
183
+ endpoint,
184
+ timeoutMs: input.timeoutMs ?? (spec.isCloud ? 8e3 : 25e3),
185
+ keepAlive: spec.driver === "ollama" ? input.keepAlive ?? "30m" : null
186
+ };
187
+ }
188
+ function isCloudProviderId(providerId) {
189
+ if (!providerId) {
190
+ return false;
191
+ }
192
+ const normalized = normalizeLegacyProviderId(providerId);
193
+ if (!normalized) {
194
+ return false;
195
+ }
196
+ return JUDGE_CATALOG[normalized].isCloud;
197
+ }
198
+
85
199
  // src/core/audit-summary.ts
86
200
  var DEFAULT_SILENT_PASS_THRESHOLD = 0.5;
87
201
 
88
202
  // src/core/config.ts
89
203
  var DEFAULT_JUDGE_LOCAL_OLLAMA = {
90
204
  provider: "ollama",
205
+ providerId: "ollama",
91
206
  model: "gemma4:e2b",
92
207
  endpoint: "http://localhost:11434",
93
208
  timeoutMs: 25e3,
@@ -95,7 +210,8 @@ var DEFAULT_JUDGE_LOCAL_OLLAMA = {
95
210
  };
96
211
  var DEFAULT_JUDGE_OPENAI_COMPATIBLE_TEMPLATE = {
97
212
  provider: "openai-compatible",
98
- model: "auto",
213
+ providerId: "codex",
214
+ model: "gpt-5.3-codex-high",
99
215
  timeoutMs: 8e3,
100
216
  endpoint: null,
101
217
  keepAlive: null
@@ -128,7 +244,7 @@ var LEGACY_POLICY_V3 = {
128
244
  fenceWarnThreshold: DEFAULT_FENCE_WARN_THRESHOLD
129
245
  };
130
246
  var DEFAULT_POLICY_V3 = {
131
- unknownLocalEffect: "deny",
247
+ unknownLocalEffect: "allow_flagged",
132
248
  unparseableShell: "deny",
133
249
  codexUnmappedTool: "deny",
134
250
  confidenceThresholds: { ...DEFAULT_CONFIDENCE_THRESHOLDS },
@@ -271,36 +387,125 @@ function migrateV2ToV3(v2, rawOverrides) {
271
387
  });
272
388
  }
273
389
  function normalizeJudgeProvider(provider) {
390
+ if (provider === "anthropic") {
391
+ return "anthropic";
392
+ }
274
393
  if (provider === "openai-compatible" || provider === "cursor") {
275
394
  return "openai-compatible";
276
395
  }
277
396
  return "ollama";
278
397
  }
398
+ function defaultJudgeTemplateForProvider(provider) {
399
+ if (provider === "ollama") {
400
+ return DEFAULT_JUDGE_LOCAL_OLLAMA;
401
+ }
402
+ if (provider === "anthropic") {
403
+ return {
404
+ provider: "anthropic",
405
+ providerId: "claude",
406
+ model: "claude-sonnet-4-6",
407
+ timeoutMs: 8e3,
408
+ endpoint: null,
409
+ keepAlive: null
410
+ };
411
+ }
412
+ return DEFAULT_JUDGE_OPENAI_COMPATIBLE_TEMPLATE;
413
+ }
279
414
  function synthesizeJudgeFromRaw(raw) {
280
415
  const judge = raw.judge;
281
416
  if (judge?.provider) {
282
- const provider = normalizeJudgeProvider(judge.provider);
283
- const base = provider === "openai-compatible" ? DEFAULT_JUDGE_OPENAI_COMPATIBLE_TEMPLATE : DEFAULT_JUDGE_LOCAL_OLLAMA;
417
+ const rawProvider = String(judge.provider);
418
+ const provider = normalizeJudgeProvider(rawProvider);
419
+ const base = defaultJudgeTemplateForProvider(provider);
420
+ const providerId = judge.providerId && normalizeLegacyProviderId(judge.providerId) ? normalizeLegacyProviderId(judge.providerId) : rawProvider === "cursor" ? "cursor" : void 0;
284
421
  return normalizeJudgeConfig({
285
422
  ...base,
286
423
  ...judge,
287
- provider
424
+ provider,
425
+ ...providerId ? { providerId } : {}
288
426
  });
289
427
  }
290
428
  return { ...DEFAULT_JUDGE_LOCAL_OLLAMA };
291
429
  }
292
430
  function normalizeJudgeConfig(judge) {
293
431
  const provider = normalizeJudgeProvider(judge.provider);
294
- const base = provider === "openai-compatible" ? DEFAULT_JUDGE_OPENAI_COMPATIBLE_TEMPLATE : DEFAULT_JUDGE_LOCAL_OLLAMA;
295
- const model = typeof judge.model === "string" && judge.model.trim() ? judge.model.trim() : base.model;
432
+ const base = defaultJudgeTemplateForProvider(provider);
433
+ const rawProviderId = judge.providerId ? String(judge.providerId) : void 0;
434
+ if (rawProviderId && isRemovedProviderId(rawProviderId)) {
435
+ warnRemovedProviderId(rawProviderId);
436
+ let model2 = typeof judge.model === "string" && judge.model.trim() ? judge.model.trim() : base.model;
437
+ if (model2 === "auto") {
438
+ warnDeprecatedJudgeModelAuto();
439
+ model2 = base.model;
440
+ }
441
+ const timeoutMs2 = typeof judge.timeoutMs === "number" && judge.timeoutMs > 0 ? judge.timeoutMs : base.timeoutMs;
442
+ const endpoint2 = typeof judge.endpoint === "string" && judge.endpoint.trim() ? judge.endpoint.trim() : null;
443
+ return {
444
+ provider,
445
+ providerId: rawProviderId,
446
+ model: model2,
447
+ timeoutMs: timeoutMs2,
448
+ endpoint: endpoint2,
449
+ keepAlive: provider === "ollama" && typeof judge.keepAlive === "string" && judge.keepAlive.trim() ? judge.keepAlive.trim() : provider === "ollama" ? DEFAULT_JUDGE_LOCAL_OLLAMA.keepAlive : null,
450
+ ...judge.cloudConsent ? { cloudConsent: judge.cloudConsent } : {},
451
+ ...judge.credential ? { credential: judge.credential } : {}
452
+ };
453
+ }
454
+ let providerId = rawProviderId && normalizeLegacyProviderId(rawProviderId) ? normalizeLegacyProviderId(rawProviderId) : inferProviderIdFromConfig({ ...judge, provider });
455
+ const spec = getJudgeProviderSpec(providerId);
456
+ if (spec && spec.driver !== provider) {
457
+ providerId = inferProviderIdFromConfig({ ...judge, provider });
458
+ }
459
+ const catalogSpec = getJudgeProviderSpec(providerId);
460
+ let model = typeof judge.model === "string" && judge.model.trim() ? judge.model.trim() : base.model;
461
+ if (model === "auto" && catalogSpec?.defaultModel) {
462
+ warnDeprecatedJudgeModelAuto();
463
+ model = catalogSpec.defaultModel;
464
+ }
465
+ if (!model && catalogSpec?.defaultModel) {
466
+ model = catalogSpec.defaultModel;
467
+ }
296
468
  const timeoutMs = typeof judge.timeoutMs === "number" && judge.timeoutMs > 0 ? judge.timeoutMs : base.timeoutMs;
297
- return {
469
+ let endpoint = typeof judge.endpoint === "string" && judge.endpoint.trim() ? judge.endpoint.trim() : null;
470
+ if (!endpoint && catalogSpec?.defaultEndpoint) {
471
+ endpoint = catalogSpec.defaultEndpoint;
472
+ }
473
+ const normalized = {
298
474
  provider,
475
+ providerId,
299
476
  model,
300
477
  timeoutMs,
301
- endpoint: typeof judge.endpoint === "string" && judge.endpoint.trim() ? judge.endpoint.trim() : null,
478
+ endpoint,
302
479
  keepAlive: provider === "ollama" && typeof judge.keepAlive === "string" && judge.keepAlive.trim() ? judge.keepAlive.trim() : provider === "ollama" ? DEFAULT_JUDGE_LOCAL_OLLAMA.keepAlive : null
303
480
  };
481
+ if (judge.cloudConsent?.accepted) {
482
+ normalized.cloudConsent = {
483
+ accepted: true,
484
+ at: judge.cloudConsent.at,
485
+ providerId: judge.cloudConsent.providerId ?? providerId,
486
+ endpoint: judge.cloudConsent.endpoint,
487
+ by: judge.cloudConsent.by
488
+ };
489
+ }
490
+ if (judge.credential?.mode === "project" || judge.credential?.mode === "apiKey") {
491
+ normalized.credential = {
492
+ mode: judge.credential.mode,
493
+ ...judge.credential.ref ? { ref: judge.credential.ref } : {}
494
+ };
495
+ }
496
+ return normalized;
497
+ }
498
+ function rejectTeamLayerJudgeSecrets(judge, source) {
499
+ if (source !== "team" || !judge) {
500
+ return;
501
+ }
502
+ if (judge.credential?.mode === "apiKey") {
503
+ throw new Error("team config cannot set judge.credential.mode to apiKey.");
504
+ }
505
+ const raw = judge;
506
+ if (raw.credential && "key" in raw.credential && raw.credential.key) {
507
+ throw new Error("team config cannot contain inline judge credential keys.");
508
+ }
304
509
  }
305
510
  function hasV3Sections(raw) {
306
511
  return raw.policy !== void 0 || raw.overrides !== void 0 || raw.redaction !== void 0 || raw.controlPlane !== void 0;
@@ -741,20 +946,29 @@ var claudeLayout = {
741
946
  // src/adapters/shared/gate-runtime.ts
742
947
  import { randomUUID as randomUUID2 } from "node:crypto";
743
948
  import { existsSync as existsSync6 } from "node:fs";
744
- import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
745
- import path16 from "node:path";
949
+ import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
950
+ import path20 from "node:path";
746
951
 
747
952
  // src/core/approval.ts
953
+ var APPROVAL_EXECUTION_LEASE_MS = 6e4;
748
954
  function nowIso() {
749
955
  return (/* @__PURE__ */ new Date()).toISOString();
750
956
  }
751
957
  function isExpired(approval) {
752
958
  return Date.parse(approval.expiresAt) <= Date.now();
753
959
  }
960
+ function isExecutionLeaseExpired(approval) {
961
+ if (!approval.executionLeaseExpiresAt) {
962
+ return false;
963
+ }
964
+ return Date.parse(approval.executionLeaseExpiresAt) <= Date.now();
965
+ }
754
966
  function compactApprovals(state) {
755
967
  return {
756
968
  version: state.version,
757
- approvals: state.approvals.filter((approval) => !isExpired(approval))
969
+ approvals: state.approvals.filter(
970
+ (approval) => !isExpired(approval) && !isExecutionLeaseExpired(approval)
971
+ )
758
972
  };
759
973
  }
760
974
  function escapeRegex(value) {
@@ -763,8 +977,15 @@ function escapeRegex(value) {
763
977
  }
764
978
  function approvalCommandMatch(prompt, tokenPrefix) {
765
979
  const escapedPrefix = escapeRegex(tokenPrefix);
766
- const match = prompt.match(new RegExp(`^\\s*${escapedPrefix}\\s+(\\S+)\\s*$`, "i"));
767
- return match?.[1] ?? null;
980
+ const linePattern = new RegExp(`^\\s*${escapedPrefix}\\s+(\\S+)\\s*$`, "i");
981
+ for (const line of prompt.split(/\r?\n/)) {
982
+ if (!line.trim()) {
983
+ continue;
984
+ }
985
+ const match = line.match(linePattern);
986
+ return match?.[1] ?? null;
987
+ }
988
+ return null;
768
989
  }
769
990
  function buildRetryInstruction(tokenPrefix, approvalId) {
770
991
  return `To allow the next matching action once, send ${tokenPrefix} ${approvalId} and then retry the original action unchanged.`;
@@ -997,6 +1218,9 @@ function isCapabilityBrokerDemotionActive(config) {
997
1218
  return hasSandboxRuntime(config);
998
1219
  }
999
1220
 
1221
+ // src/core/capability/paths.ts
1222
+ import path7 from "node:path";
1223
+
1000
1224
  // src/core/shell-tokenizer.ts
1001
1225
  function tokenizeShell(input) {
1002
1226
  const tokens = [];
@@ -1094,33 +1318,106 @@ function extractRedirectTargets(tokens) {
1094
1318
  }
1095
1319
 
1096
1320
  // src/core/capability/paths.ts
1321
+ function applyPatchTargets(patch) {
1322
+ const targets = [];
1323
+ for (const line of patch.split("\n")) {
1324
+ const match = line.match(/^\*\*\* (?:Add|Delete|Update) File: (.+)$/);
1325
+ if (match?.[1]) {
1326
+ targets.push(match[1]);
1327
+ continue;
1328
+ }
1329
+ const moveMatch = line.match(/^\*\*\* Move to: (.+)$/);
1330
+ if (moveMatch?.[1]) {
1331
+ targets.push(moveMatch[1]);
1332
+ }
1333
+ }
1334
+ return targets;
1335
+ }
1336
+ function extractToolFilePath(payload) {
1337
+ const toolInput = payload.tool_input;
1338
+ if (!toolInput || typeof toolInput !== "object") {
1339
+ return null;
1340
+ }
1341
+ const input = toolInput;
1342
+ for (const key of ["path", "file_path", "target_file", "filePath"]) {
1343
+ if (typeof input[key] === "string") {
1344
+ return input[key];
1345
+ }
1346
+ }
1347
+ return null;
1348
+ }
1349
+ function extractToolPatch(payload) {
1350
+ const toolInput = payload.tool_input;
1351
+ if (!toolInput || typeof toolInput !== "object") {
1352
+ return null;
1353
+ }
1354
+ const input = toolInput;
1355
+ for (const key of ["patch", "input", "text"]) {
1356
+ if (typeof input[key] === "string" && input[key].trim()) {
1357
+ return input[key];
1358
+ }
1359
+ }
1360
+ return null;
1361
+ }
1362
+ function addOutsideRepoPath(paths, token, cwd, repoRoot) {
1363
+ const resolved = resolveMutationTarget(token, cwd);
1364
+ if (resolved && relativeWithinRepo(repoRoot, resolved) === null) {
1365
+ paths.add(resolved);
1366
+ }
1367
+ }
1097
1368
  function collectOutsideRepoPaths(command, cwd, repoRoot) {
1098
1369
  const tokens = tokenizeShell(command);
1099
1370
  const redirects = extractRedirectTargets(tokens);
1100
1371
  const paths = /* @__PURE__ */ new Set();
1101
1372
  for (const token of tokens.slice(1)) {
1102
- const resolved = resolveMutationTarget(token, cwd);
1103
- if (resolved && relativeWithinRepo(repoRoot, resolved) === null) {
1104
- paths.add(resolved);
1105
- }
1373
+ addOutsideRepoPath(paths, token, cwd, repoRoot);
1106
1374
  }
1107
1375
  for (const redirect of redirects) {
1108
- const resolved = resolveMutationTarget(redirect, cwd);
1109
- if (resolved && relativeWithinRepo(repoRoot, resolved) === null) {
1376
+ addOutsideRepoPath(paths, redirect, cwd, repoRoot);
1377
+ }
1378
+ return [...paths];
1379
+ }
1380
+ function collectOutsideRepoPathsFromToolPayload(payload, cwd, repoRoot) {
1381
+ const toolKind = String(payload.tool_name ?? "").trim().toLowerCase();
1382
+ const paths = /* @__PURE__ */ new Set();
1383
+ const filePath = extractToolFilePath(payload);
1384
+ if (filePath) {
1385
+ const resolved = path7.isAbsolute(filePath) ? filePath : path7.resolve(cwd, filePath);
1386
+ if (relativeWithinRepo(repoRoot, resolved) === null) {
1110
1387
  paths.add(resolved);
1111
1388
  }
1389
+ return [...paths];
1390
+ }
1391
+ if (toolKind === "apply_patch" || toolKind === "applypatch") {
1392
+ const patch = extractToolPatch(payload);
1393
+ if (patch) {
1394
+ for (const target of applyPatchTargets(patch)) {
1395
+ const resolved = path7.isAbsolute(target) ? target : path7.resolve(cwd, target);
1396
+ if (relativeWithinRepo(repoRoot, resolved) === null) {
1397
+ paths.add(resolved);
1398
+ }
1399
+ }
1400
+ }
1112
1401
  }
1113
1402
  return [...paths];
1114
1403
  }
1115
1404
 
1116
1405
  // src/core/capability/reasons.ts
1117
1406
  var FS_SCOPE_REASONS = /* @__PURE__ */ new Set(["outside_repo_mutation", "outside_repo_redirect"]);
1407
+ var JUDGE_CLOUD_CONSENT_REASON = "judge_cloud_consent";
1408
+ var CAPABILITY_APPROVAL_REASONS = /* @__PURE__ */ new Set([
1409
+ ...FS_SCOPE_REASONS,
1410
+ JUDGE_CLOUD_CONSENT_REASON
1411
+ ]);
1118
1412
  function shouldSkipBrokerApprovedOnce(brokerActive, reason) {
1119
1413
  return brokerActive && FS_SCOPE_REASONS.has(reason);
1120
1414
  }
1415
+ function shouldSkipBrokerApprovedRecord(brokerActive, approvalReason) {
1416
+ return brokerActive && approvalReason !== void 0 && FS_SCOPE_REASONS.has(approvalReason);
1417
+ }
1121
1418
 
1122
1419
  // src/core/config-layers.ts
1123
- import path7 from "node:path";
1420
+ import path8 from "node:path";
1124
1421
 
1125
1422
  // src/presets.ts
1126
1423
  var CONFIG_PRESETS = {
@@ -1220,8 +1517,8 @@ function applyConfigPreset(preset, extra = {}) {
1220
1517
  // src/core/config-layers.ts
1221
1518
  function teamConfigPath(homedir = () => process.env.HOME ?? process.env.USERPROFILE ?? "") {
1222
1519
  const xdg = process.env.XDG_CONFIG_HOME?.trim();
1223
- const base = xdg || path7.join(homedir(), ".config");
1224
- return path7.join(base, "agent-belay", "team.config.json");
1520
+ const base = xdg || path8.join(homedir(), ".config");
1521
+ return path8.join(base, "agent-belay", "team.config.json");
1225
1522
  }
1226
1523
  function applyProtectedLayer(config, builtin) {
1227
1524
  const controlPlane = { ...config.controlPlane };
@@ -1255,6 +1552,10 @@ function resolveLayeredConfig(params) {
1255
1552
  if (params.teamConfig) {
1256
1553
  const teamFile = params.teamConfig;
1257
1554
  const teamRaw = teamFile.preset ? applyConfigPreset(teamFile.preset, teamFile.config ?? {}) : teamFile.config ?? params.teamConfig;
1555
+ rejectTeamLayerJudgeSecrets(
1556
+ teamRaw.judge,
1557
+ "team"
1558
+ );
1258
1559
  config = mergeConfigLayer(config, asV3Layer(teamRaw));
1259
1560
  provenance.push({
1260
1561
  path: params.teamConfigPath ?? teamConfigPath(),
@@ -1458,7 +1759,7 @@ function classifySubagent(payload, repoRoot, options = {}) {
1458
1759
  }
1459
1760
 
1460
1761
  // src/core/classify-tool.ts
1461
- import path12 from "node:path";
1762
+ import path16 from "node:path";
1462
1763
 
1463
1764
  // src/core/glob.ts
1464
1765
  function matchesSensitivePath(filePath, patterns) {
@@ -1503,7 +1804,7 @@ function matchesSensitivePath(filePath, patterns) {
1503
1804
  return false;
1504
1805
  }
1505
1806
 
1506
- // src/core/v2/judge-audit.ts
1807
+ // src/core/verdict/judge-audit.ts
1507
1808
  function judgeTraceAuditFields(trace) {
1508
1809
  if (!trace) {
1509
1810
  return {};
@@ -1518,55 +1819,465 @@ function judgeTraceAuditFields(trace) {
1518
1819
  };
1519
1820
  }
1520
1821
 
1521
- // src/core/v2/judge-outbound.ts
1522
- var PATH_LIKE = /(?:^|[\s"'`=])(~\/[^\s"'`]+|\/[^\s"'`]+|\.\/[^\s"'`]+|\.\.\/[^\s"'`]+|[A-Za-z]:\\[^\s"'`]+)/g;
1523
- var REDACTED_PLACEHOLDER = /^(?:<redacted>|\[REDACTED\]|<secret>|<high-entropy>|<approval-id>)$/i;
1524
- var URL_CREDENTIALS_PATTERN2 = /\b[A-Za-z][A-Za-z0-9+.-]*:\/\/([^/\s:@]+):([^@\s/]+)@/gi;
1525
- var GENERIC_AUTH_HEADER_PATTERN2 = /\b(?:Authorization|X-Api-Key|X-Auth-Token|Private-Token):\s*([^\s]+)/gi;
1526
- function hasResidualBearerToken(text) {
1527
- for (const match of text.matchAll(/\bBearer\s+(\S+)/gi)) {
1528
- const token = match[1] ?? "";
1529
- if (!REDACTED_PLACEHOLDER.test(token)) {
1530
- return true;
1531
- }
1822
+ // src/adapters/layouts/codex.ts
1823
+ import path9 from "node:path";
1824
+ function runnerCommand2(platform, repoRoot, hookName, ...args) {
1825
+ const hooksDir = path9.join(path9.resolve(repoRoot), ".codex", "hooks");
1826
+ return buildRunnerInvocation(platform, hooksDir, repoRoot, hookName, ...args);
1827
+ }
1828
+ var codexLayout = {
1829
+ name: "codex",
1830
+ configPath(repoRoot) {
1831
+ return path9.join(repoRoot, ".codex", "belay.config.json");
1832
+ },
1833
+ // Codex reads lifecycle hooks from `.codex/config.toml` (project layer).
1834
+ hooksSettingsPath(repoRoot) {
1835
+ return path9.join(repoRoot, ".codex", "config.toml");
1836
+ },
1837
+ hooksDir(repoRoot) {
1838
+ return path9.join(repoRoot, ".codex", "hooks");
1839
+ },
1840
+ runtimeDir(repoRoot) {
1841
+ return path9.join(repoRoot, ".codex", "belay", "runtime");
1842
+ },
1843
+ repoLocalStateDir(repoRoot) {
1844
+ return path9.join(repoRoot, ".codex", "belay");
1845
+ },
1846
+ defaultAuditLogPath(_repoRoot) {
1847
+ return path9.join(".codex", "belay", "audit.ndjson");
1848
+ },
1849
+ repoRootMarkers: [".git", ".codex"],
1850
+ runnerCommand: runnerCommand2,
1851
+ defaultConfig(repoRoot) {
1852
+ return {
1853
+ ...DEFAULT_CONFIG_V4,
1854
+ adapter: "codex",
1855
+ audit: {
1856
+ ...DEFAULT_CONFIG_V4.audit,
1857
+ logPath: codexLayout.defaultAuditLogPath(repoRoot)
1858
+ }
1859
+ };
1532
1860
  }
1533
- return false;
1861
+ };
1862
+
1863
+ // src/adapters/layouts/cursor.ts
1864
+ import path10 from "node:path";
1865
+ function runnerCommand3(platform, repoRoot, hookName, ...args) {
1866
+ const hooksDir = path10.join(path10.resolve(repoRoot), ".cursor", "hooks");
1867
+ return buildRunnerInvocation(platform, hooksDir, repoRoot, hookName, ...args);
1534
1868
  }
1535
- function hasResidualApiKey(text) {
1536
- return /\bsk-(?![^\s]*<redacted>)[A-Za-z0-9_-]{4,}/i.test(text);
1869
+ var cursorLayout = {
1870
+ name: "cursor",
1871
+ configPath(repoRoot) {
1872
+ return path10.join(repoRoot, ".cursor", "belay.config.json");
1873
+ },
1874
+ hooksSettingsPath(repoRoot) {
1875
+ return path10.join(repoRoot, ".cursor", "hooks.json");
1876
+ },
1877
+ hooksDir(repoRoot) {
1878
+ return path10.join(repoRoot, ".cursor", "hooks");
1879
+ },
1880
+ runtimeDir(repoRoot) {
1881
+ return path10.join(repoRoot, ".cursor", "belay", "runtime");
1882
+ },
1883
+ repoLocalStateDir(repoRoot) {
1884
+ return path10.join(repoRoot, ".cursor", "belay");
1885
+ },
1886
+ defaultAuditLogPath(_repoRoot) {
1887
+ return path10.join(".cursor", "belay", "audit.ndjson");
1888
+ },
1889
+ repoRootMarkers: [".git", ".cursor"],
1890
+ runnerCommand: runnerCommand3,
1891
+ defaultConfig(repoRoot) {
1892
+ return {
1893
+ ...DEFAULT_CONFIG_V4,
1894
+ adapter: "cursor",
1895
+ audit: {
1896
+ ...DEFAULT_CONFIG_V4.audit,
1897
+ logPath: cursorLayout.defaultAuditLogPath(repoRoot)
1898
+ }
1899
+ };
1900
+ }
1901
+ };
1902
+
1903
+ // src/adapters/layouts/index.ts
1904
+ var layouts = {
1905
+ cursor: cursorLayout,
1906
+ claude: claudeLayout,
1907
+ codex: codexLayout
1908
+ };
1909
+ function getAdapterLayout(name = "cursor") {
1910
+ return layouts[name];
1537
1911
  }
1538
- function hasResidualUrlCredentials(text) {
1539
- for (const match of text.matchAll(URL_CREDENTIALS_PATTERN2)) {
1540
- const username = (match[1] ?? "").replace(/^['"]|['"]$/g, "");
1541
- const password = (match[2] ?? "").replace(/^['"]|['"]$/g, "");
1542
- if (!REDACTED_PLACEHOLDER.test(username) || !REDACTED_PLACEHOLDER.test(password)) {
1543
- return true;
1544
- }
1912
+
1913
+ // src/config-io.ts
1914
+ function resolveAdapterName(config) {
1915
+ if (config.adapter === "claude") {
1916
+ return "claude";
1545
1917
  }
1546
- return false;
1918
+ if (config.adapter === "codex") {
1919
+ return "codex";
1920
+ }
1921
+ return "cursor";
1547
1922
  }
1548
- function hasResidualAuthHeader(text) {
1549
- for (const match of text.matchAll(GENERIC_AUTH_HEADER_PATTERN2)) {
1550
- const token = (match[1] ?? "").replace(/^['"]|['"]$/g, "");
1551
- if (!REDACTED_PLACEHOLDER.test(token)) {
1923
+ function repoLocalStateDirFor(repoRoot, config) {
1924
+ return getAdapterLayout(resolveAdapterName(config)).repoLocalStateDir(repoRoot);
1925
+ }
1926
+
1927
+ // src/core/credential-store.ts
1928
+ import { chmod, mkdir as mkdir2, readFile as readFile2, unlink, writeFile as writeFile2 } from "node:fs/promises";
1929
+ import path11 from "node:path";
1930
+ var STORE_FILENAME = "credentials.json";
1931
+ function credentialStorePath(stateDir) {
1932
+ return path11.join(stateDir, STORE_FILENAME);
1933
+ }
1934
+ async function readJudgeCredentialStore(stateDir) {
1935
+ const filePath = credentialStorePath(stateDir);
1936
+ try {
1937
+ const raw = await readFile2(filePath, "utf8");
1938
+ const parsed = JSON.parse(raw);
1939
+ const key = parsed.judge?.trim();
1940
+ return key || null;
1941
+ } catch {
1942
+ return null;
1943
+ }
1944
+ }
1945
+ function parseCredentialRef(ref) {
1946
+ if (ref === "store:judge") {
1947
+ return ref;
1948
+ }
1949
+ if (ref.startsWith("env:") && ref.length > 4) {
1950
+ return ref;
1951
+ }
1952
+ return null;
1953
+ }
1954
+
1955
+ // src/core/judge-runtime-detection.ts
1956
+ import { accessSync, constants } from "node:fs";
1957
+ import path12 from "node:path";
1958
+ var CLI_COMMANDS = {
1959
+ codex: "codex",
1960
+ cursor: "cursor-agent",
1961
+ claude: "claude"
1962
+ };
1963
+ function isVitestRuntime(env) {
1964
+ return Boolean(env.VITEST || env.VITEST_WORKER_ID);
1965
+ }
1966
+ function cliTransportForProvider(providerId) {
1967
+ if (providerId === "ollama") {
1968
+ return null;
1969
+ }
1970
+ return `${providerId}-cli`;
1971
+ }
1972
+ function commandOnPath(command, env) {
1973
+ const pathValue = env.PATH ?? "";
1974
+ for (const dir of pathValue.split(path12.delimiter).filter(Boolean)) {
1975
+ const candidate = path12.join(dir, command);
1976
+ try {
1977
+ accessSync(candidate, constants.X_OK);
1552
1978
  return true;
1979
+ } catch {
1553
1980
  }
1554
1981
  }
1555
1982
  return false;
1556
1983
  }
1557
- function redactSensitivePathToken(token, sensitivePaths) {
1558
- const trimmed = token.replace(/^['"`]+|['"`]+$/g, "");
1559
- const normalized = trimmed.replaceAll("\\", "/");
1560
- if (!matchesSensitivePath(normalized, sensitivePaths)) {
1561
- return token;
1984
+ function detectJudgeRuntimeCapabilities(providerId, env = process.env) {
1985
+ const normalized = normalizeLegacyProviderId(String(providerId));
1986
+ if (!normalized || normalized === "ollama") {
1987
+ return { http: false, cliTransport: null };
1562
1988
  }
1563
- const segments = normalized.split("/");
1564
- const basename = segments.at(-1) ?? normalized;
1565
- if (segments.length > 1) {
1566
- return token.replace(basename, "[REDACTED]");
1989
+ if (env.BELAY_JUDGE_DISABLE_CLI_TRANSPORT === "1") {
1990
+ return { http: true, cliTransport: null };
1567
1991
  }
1568
- return "[REDACTED]";
1569
- }
1992
+ if (isVitestRuntime(env)) {
1993
+ return { http: false, cliTransport: cliTransportForProvider(normalized) };
1994
+ }
1995
+ const command = CLI_COMMANDS[normalized];
1996
+ if (commandOnPath(command, env)) {
1997
+ return { http: false, cliTransport: cliTransportForProvider(normalized) };
1998
+ }
1999
+ return { http: true, cliTransport: null };
2000
+ }
2001
+
2002
+ // src/core/judge-api-key.ts
2003
+ function resolveFromEnvChain(env, catalogSpec) {
2004
+ const belay = env.BELAY_JUDGE_API_KEY?.trim();
2005
+ if (belay) {
2006
+ return { key: belay, source: "BELAY_JUDGE_API_KEY" };
2007
+ }
2008
+ for (const name of catalogSpec?.apiKeyEnvVars ?? ["OPENAI_API_KEY", "BELAY_JUDGE_API_KEY"]) {
2009
+ const value = env[name]?.trim();
2010
+ if (value) {
2011
+ return { key: value, source: name };
2012
+ }
2013
+ }
2014
+ return { key: null, source: null };
2015
+ }
2016
+ function sourceKindForProjectMode(fromEnv, judge, catalogSpec) {
2017
+ if (fromEnv.key) {
2018
+ return "env";
2019
+ }
2020
+ const providerId = judge.providerId && normalizeLegacyProviderId(judge.providerId) ? normalizeLegacyProviderId(judge.providerId) : inferProviderIdFromConfig(judge);
2021
+ const spec = catalogSpec ?? getJudgeProviderSpec(providerId);
2022
+ if (spec?.isCloud && detectJudgeRuntimeCapabilities(providerId).cliTransport) {
2023
+ return "host-session";
2024
+ }
2025
+ return "missing";
2026
+ }
2027
+ function sourceKindForApiKeyMode(key, source) {
2028
+ if (!key) {
2029
+ return "missing";
2030
+ }
2031
+ if (source?.startsWith("env:")) {
2032
+ return "env";
2033
+ }
2034
+ return "store";
2035
+ }
2036
+ async function resolveJudgeCredential(params) {
2037
+ const env = params.env ?? process.env;
2038
+ const credential = params.judge.credential ?? { mode: "project" };
2039
+ if (credential.mode === "apiKey") {
2040
+ const ref = credential.ref ? parseCredentialRef(credential.ref) : "store:judge";
2041
+ if (ref === "store:judge") {
2042
+ const stateDir = belayStateDir(params.config, params.repoLocalStateDir);
2043
+ const key = await readJudgeCredentialStore(stateDir);
2044
+ return {
2045
+ key,
2046
+ source: key ? `${stateDir}/credentials.json` : null,
2047
+ sourceKind: sourceKindForApiKeyMode(key, key ? `${stateDir}/credentials.json` : null),
2048
+ mode: "apiKey"
2049
+ };
2050
+ }
2051
+ if (ref?.startsWith("env:")) {
2052
+ const envName = ref.slice(4);
2053
+ const value = env[envName]?.trim() ?? null;
2054
+ return {
2055
+ key: value,
2056
+ source: value ? `env:${envName}` : null,
2057
+ sourceKind: sourceKindForApiKeyMode(value, value ? `env:${envName}` : null),
2058
+ mode: "apiKey"
2059
+ };
2060
+ }
2061
+ return { key: null, source: null, sourceKind: "missing", mode: "apiKey" };
2062
+ }
2063
+ const fromEnv = resolveFromEnvChain(env, params.catalogSpec);
2064
+ return {
2065
+ ...fromEnv,
2066
+ sourceKind: sourceKindForProjectMode(fromEnv, params.judge, params.catalogSpec),
2067
+ mode: "project"
2068
+ };
2069
+ }
2070
+
2071
+ // src/core/judge-config.ts
2072
+ var JUDGE_PROFILE_LOCAL_OLLAMA = resolveJudgeFromCatalog({
2073
+ providerId: "ollama"
2074
+ });
2075
+ var JUDGE_PROFILE_CURSOR = resolveJudgeFromCatalog({
2076
+ providerId: "cursor"
2077
+ });
2078
+ var JUDGE_PROFILE_CLAUDE = resolveJudgeFromCatalog({
2079
+ providerId: "claude"
2080
+ });
2081
+ var JUDGE_PROFILE_CODEX = resolveJudgeFromCatalog({
2082
+ providerId: "codex"
2083
+ });
2084
+ function isCloudJudgeConfig(judge) {
2085
+ if (judge.providerId) {
2086
+ return isCloudProviderId(judge.providerId);
2087
+ }
2088
+ return judge.provider === "openai-compatible" || judge.provider === "anthropic";
2089
+ }
2090
+ function hasValidCloudConsent(judge) {
2091
+ if (!judge.cloudConsent?.accepted) {
2092
+ return false;
2093
+ }
2094
+ if (!judge.endpoint?.trim()) {
2095
+ return false;
2096
+ }
2097
+ const providerId = judge.providerId && normalizeLegacyProviderId(judge.providerId) ? normalizeLegacyProviderId(judge.providerId) : judge.provider === "ollama" ? "ollama" : "codex";
2098
+ return judge.cloudConsent.endpoint === judge.endpoint.trim() && normalizeLegacyProviderId(judge.cloudConsent.providerId) === providerId;
2099
+ }
2100
+
2101
+ // src/core/verdict/containment.ts
2102
+ import path13 from "node:path";
2103
+
2104
+ // src/core/verdict/persistent-paths.ts
2105
+ function isPersistentAgentPath(resolvedPath) {
2106
+ const normalized = resolvedPath.replaceAll("\\", "/");
2107
+ return /\.ssh\/authorized_keys$/i.test(normalized) || /\/\.(zshrc|bashrc|bash_profile|profile)$/i.test(normalized) || /\/\.config\//i.test(normalized) || /\/crontab$/i.test(normalized) || /LaunchAgents\//i.test(normalized) || /\/Library\/LaunchDaemons\//i.test(normalized);
2108
+ }
2109
+ function isOutsideRepoSecretCredentialPath(resolvedPath) {
2110
+ const normalized = resolvedPath.replaceAll("\\", "/");
2111
+ return /\/\.env(?:\.[^/]+)?$/i.test(normalized) || /\/[^/]+\.pem$/i.test(normalized) || /\/\.ssh\/id_[^/]+$/i.test(normalized) || /\/\.aws\//i.test(normalized) || /\/\.npmrc$/i.test(normalized) || /\/\.git-credentials$/i.test(normalized) || /\/\.netrc$/i.test(normalized) || /\/\.kube\/config$/i.test(normalized) || /\/\.docker\/config\.json$/i.test(normalized) || /\/\.gnupg\//i.test(normalized) || /\/\.pypirc$/i.test(normalized);
2112
+ }
2113
+
2114
+ // src/core/verdict/containment.ts
2115
+ function expandHome(token) {
2116
+ if (token === "~" || token.startsWith("~/")) {
2117
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
2118
+ if (!home) {
2119
+ return token;
2120
+ }
2121
+ return token === "~" ? home : path13.join(home, token.slice(2));
2122
+ }
2123
+ return token;
2124
+ }
2125
+ function resolveTrustedPath(token, trustedCwd, trusted) {
2126
+ if (!token || token === "--" || token.startsWith("-")) {
2127
+ return null;
2128
+ }
2129
+ if (!trusted || !trustedCwd) {
2130
+ return null;
2131
+ }
2132
+ const expanded = expandHome(token);
2133
+ if (path13.isAbsolute(expanded)) {
2134
+ return canonicalPath(expanded);
2135
+ }
2136
+ return canonicalPath(path13.resolve(trustedCwd, expanded));
2137
+ }
2138
+ function locationForPath(resolvedPath, repoRoot) {
2139
+ if (!resolvedPath) {
2140
+ return "unknown";
2141
+ }
2142
+ if (pathWithinRoot(repoRoot, resolvedPath)) {
2143
+ return "repo_local";
2144
+ }
2145
+ return "repo_outside";
2146
+ }
2147
+ function isGitPath(resolvedPath, repoRoot) {
2148
+ const relative = relativeWithinRepo(repoRoot, resolvedPath);
2149
+ if (!relative) {
2150
+ return false;
2151
+ }
2152
+ const normalized = relative.replaceAll("\\", "/");
2153
+ return normalized === ".git" || normalized.startsWith(".git/");
2154
+ }
2155
+ function isHighStakesPath(resolvedPath, repoRoot, sensitivePaths, protectedRoots = []) {
2156
+ if (isGitPath(resolvedPath, repoRoot)) {
2157
+ return true;
2158
+ }
2159
+ const relative = relativeWithinRepo(repoRoot, resolvedPath);
2160
+ if (relative !== null && matchesSensitivePath(relative.replaceAll("\\", "/"), sensitivePaths)) {
2161
+ return true;
2162
+ }
2163
+ if (relative === null && isOutsideRepoSecretCredentialPath(resolvedPath)) {
2164
+ return true;
2165
+ }
2166
+ return protectedRoots.some((root) => pathWithinRoot(root, resolvedPath));
2167
+ }
2168
+ var DESTRUCTIVE_MUTATION_HEADS = /* @__PURE__ */ new Set([
2169
+ "rm",
2170
+ "shred",
2171
+ "truncate",
2172
+ "dd",
2173
+ "chmod",
2174
+ "chown",
2175
+ "mv",
2176
+ "cp"
2177
+ ]);
2178
+ function isDestructiveMutationHead(head) {
2179
+ return DESTRUCTIVE_MUTATION_HEADS.has(head);
2180
+ }
2181
+ function touchesProtectedRoot(resolvedPath, protectedRoots) {
2182
+ return protectedRoots.some((root) => pathWithinRoot(root, resolvedPath) || root === resolvedPath);
2183
+ }
2184
+ function isDestructiveHighStakesMutation(head, resolvedPath, repoRoot, sensitivePaths, protectedRoots = []) {
2185
+ if (touchesProtectedRoot(resolvedPath, protectedRoots)) {
2186
+ return true;
2187
+ }
2188
+ if (!isDestructiveMutationHead(head)) {
2189
+ return false;
2190
+ }
2191
+ return isHighStakesPath(resolvedPath, repoRoot, sensitivePaths, []);
2192
+ }
2193
+ function analyzePathTargets(params) {
2194
+ const signals = [];
2195
+ if (!params.trustedCwd || !params.cwd) {
2196
+ return {
2197
+ location: "unknown",
2198
+ isHighStakes: false,
2199
+ signals: ["missing_trusted_cwd"]
2200
+ };
2201
+ }
2202
+ const locations = /* @__PURE__ */ new Set();
2203
+ let isHighStakes = false;
2204
+ for (const target of params.targets) {
2205
+ const resolved = resolveTrustedPath(target, params.cwd, params.trustedCwd) ?? resolveMutationTarget(target, params.cwd);
2206
+ const location2 = locationForPath(resolved, params.repoRoot);
2207
+ locations.add(location2);
2208
+ if (resolved && isHighStakesPath(
2209
+ resolved,
2210
+ params.repoRoot,
2211
+ params.sensitivePaths,
2212
+ params.protectedArtifactRoots
2213
+ )) {
2214
+ isHighStakes = true;
2215
+ signals.push("high_stakes_path");
2216
+ }
2217
+ }
2218
+ let location = "unknown";
2219
+ if (locations.size === 0) {
2220
+ location = "unknown";
2221
+ } else if (locations.size === 1) {
2222
+ location = [...locations][0] ?? "unknown";
2223
+ } else {
2224
+ location = "mixed";
2225
+ }
2226
+ return { location, isHighStakes, signals };
2227
+ }
2228
+ function cwdRelative(repoRoot, cwd) {
2229
+ return relativeWithinRepo(repoRoot, cwd) ?? cwd;
2230
+ }
2231
+
2232
+ // src/core/verdict/judge-outbound.ts
2233
+ var PATH_LIKE = /(?:^|[\s"'`=])(~\/[^\s"'`]+|\/[^\s"'`]+|\.\/[^\s"'`]+|\.\.\/[^\s"'`]+|[A-Za-z]:\\[^\s"'`]+)/g;
2234
+ var REDACTED_PLACEHOLDER = /^(?:<redacted>|\[REDACTED\]|<secret>|<high-entropy>|<approval-id>)$/i;
2235
+ var URL_CREDENTIALS_PATTERN2 = /\b[A-Za-z][A-Za-z0-9+.-]*:\/\/([^/\s:@]+):([^@\s/]+)@/gi;
2236
+ var GENERIC_AUTH_HEADER_PATTERN2 = /\b(?:Authorization|X-Api-Key|X-Auth-Token|Private-Token):\s*([^\s]+)/gi;
2237
+ function hasResidualBearerToken(text) {
2238
+ for (const match of text.matchAll(/\bBearer\s+(\S+)/gi)) {
2239
+ const token = match[1] ?? "";
2240
+ if (!REDACTED_PLACEHOLDER.test(token)) {
2241
+ return true;
2242
+ }
2243
+ }
2244
+ return false;
2245
+ }
2246
+ function hasResidualApiKey(text) {
2247
+ return /\bsk-(?![^\s]*<redacted>)[A-Za-z0-9_-]{4,}/i.test(text);
2248
+ }
2249
+ function hasResidualUrlCredentials(text) {
2250
+ for (const match of text.matchAll(URL_CREDENTIALS_PATTERN2)) {
2251
+ const username = (match[1] ?? "").replace(/^['"]|['"]$/g, "");
2252
+ const password = (match[2] ?? "").replace(/^['"]|['"]$/g, "");
2253
+ if (!REDACTED_PLACEHOLDER.test(username) || !REDACTED_PLACEHOLDER.test(password)) {
2254
+ return true;
2255
+ }
2256
+ }
2257
+ return false;
2258
+ }
2259
+ function hasResidualAuthHeader(text) {
2260
+ for (const match of text.matchAll(GENERIC_AUTH_HEADER_PATTERN2)) {
2261
+ const token = (match[1] ?? "").replace(/^['"]|['"]$/g, "");
2262
+ if (!REDACTED_PLACEHOLDER.test(token)) {
2263
+ return true;
2264
+ }
2265
+ }
2266
+ return false;
2267
+ }
2268
+ function redactSensitivePathToken(token, sensitivePaths) {
2269
+ const trimmed = token.replace(/^['"`]+|['"`]+$/g, "");
2270
+ const normalized = trimmed.replaceAll("\\", "/");
2271
+ if (!matchesSensitivePath(normalized, sensitivePaths)) {
2272
+ return token;
2273
+ }
2274
+ const segments = normalized.split("/");
2275
+ const basename = segments.at(-1) ?? normalized;
2276
+ if (segments.length > 1) {
2277
+ return token.replace(basename, "[REDACTED]");
2278
+ }
2279
+ return "[REDACTED]";
2280
+ }
1570
2281
  function scrubOutboundForJudge(text, options) {
1571
2282
  try {
1572
2283
  let scrubbed = scrubString(text, {
@@ -1587,37 +2298,57 @@ function scrubOutboundForJudge(text, options) {
1587
2298
  }
1588
2299
  }
1589
2300
 
1590
- // src/core/v2/judge.ts
2301
+ // src/core/verdict/judge.ts
1591
2302
  var SECRET_PATTERNS = [/\.env\b/i, /\.pem\b/i, /id_rsa\b/i, /credentials/i, /secrets?\b/i];
1592
2303
  var DESTRUCTIVE_VERBS = /\b(rm|rmtree|unlink|delete|truncate|shred|destroy|drop)\b/i;
1593
2304
  var GIT_PATTERNS = /\.git\b/i;
1594
- var TIER1_PROMPT = `Answer ONLY with JSON: {"external_change":boolean,"destroys_outside_repo":boolean,"destroys_history_or_secrets":boolean,"reason":string}
1595
- Default all three booleans to true when unsure. reason should be a short snake_case label.
2305
+ var TIER1_PROMPT = `Answer ONLY with JSON: {"local_recoverable":boolean,"destroys_history_or_secrets":boolean,"reason":string}
2306
+ reason should be a short snake_case label.
2307
+
2308
+ local_recoverable: true if a mistaken operation can be undone locally (git revert, fs snapshot, trivial manual revert).
2309
+ local_recoverable: false if remote/destructive (git push --force, aws s3 rm, secret send) OR persistent local harm (~/.ssh/authorized_keys, shell rc, crontab, ~/.config persistence).
2310
+
2311
+ destroys_history_or_secrets: true if the operation deletes or corrupts git history (.git), overwrites secrets/credentials, or modifies persistent agent startup (shell rc, crontab, launchd, authorized_keys).
1596
2312
 
1597
- external_change: CREATE/MODIFY/DELETE on database, network endpoint, cloud, remote git, registry, queue. READ/QUERY/GET is NOT a change.
1598
- destroys_outside_repo: DELETE/OVERWRITE files OUTSIDE the current repository working tree.
1599
- destroys_history_or_secrets: DELETE/OVERWRITE .git directory OR secret/credential files (.env, *.pem, id_rsa, credentials).
2313
+ Examples local_recoverable=true, destroys_history_or_secrets=false: IDE plan files under the user home, /tmp files, in-repo edits, READ/QUERY/GET.
2314
+ Examples local_recoverable=false OR destroys_history_or_secrets=true: remote mutations, credential paths, agent startup persistence, rm -rf .git.
2315
+ If genuinely ambiguous about recoverability or persistent harm, answer false.
1600
2316
 
1601
2317
  Command/code:
1602
2318
  `;
1603
2319
  function failClosedVerdict(reason) {
1604
2320
  return {
1605
- external_change: true,
2321
+ local_recoverable: false,
1606
2322
  destroys_outside_repo: true,
1607
2323
  destroys_history_or_secrets: true,
2324
+ external_change: true,
1608
2325
  reason
1609
2326
  };
1610
2327
  }
2328
+ function parseLocalRecoverable(parsed) {
2329
+ if (typeof parsed.local_recoverable === "boolean") {
2330
+ return parsed.local_recoverable;
2331
+ }
2332
+ if (typeof parsed.external_change === "boolean") {
2333
+ return !parsed.external_change;
2334
+ }
2335
+ return null;
2336
+ }
2337
+ function buildTier1Prompt(text) {
2338
+ return `${TIER1_PROMPT}${text}`;
2339
+ }
1611
2340
  function parseTier1Json(raw) {
1612
2341
  try {
1613
2342
  const parsed = JSON.parse(raw);
1614
- if (typeof parsed.external_change !== "boolean" || typeof parsed.destroys_outside_repo !== "boolean" || typeof parsed.destroys_history_or_secrets !== "boolean") {
2343
+ const localRecoverable = parseLocalRecoverable(parsed);
2344
+ if (localRecoverable === null) {
1615
2345
  return null;
1616
2346
  }
1617
2347
  return {
1618
- external_change: parsed.external_change !== false,
1619
- destroys_outside_repo: parsed.destroys_outside_repo !== false,
1620
- destroys_history_or_secrets: parsed.destroys_history_or_secrets !== false,
2348
+ local_recoverable: localRecoverable,
2349
+ destroys_outside_repo: typeof parsed.destroys_outside_repo === "boolean" ? parsed.destroys_outside_repo : false,
2350
+ destroys_history_or_secrets: typeof parsed.destroys_history_or_secrets === "boolean" ? parsed.destroys_history_or_secrets : false,
2351
+ external_change: parsed.external_change,
1621
2352
  reason: typeof parsed.reason === "string" ? parsed.reason : "tier1_llm"
1622
2353
  };
1623
2354
  } catch {
@@ -1631,7 +2362,7 @@ function prescanInterpreterCode(code) {
1631
2362
  const hitsDestructive = DESTRUCTIVE_VERBS.test(normalized);
1632
2363
  if ((hitsSecret || hitsGit) && hitsDestructive) {
1633
2364
  return {
1634
- external_change: false,
2365
+ local_recoverable: true,
1635
2366
  destroys_outside_repo: false,
1636
2367
  destroys_history_or_secrets: true,
1637
2368
  reason: "prescan_destructive_secret"
@@ -1639,11 +2370,49 @@ function prescanInterpreterCode(code) {
1639
2370
  }
1640
2371
  return null;
1641
2372
  }
2373
+ function prescanMutationTargets(params) {
2374
+ for (const target of params.targets) {
2375
+ const resolved = resolveTrustedPath(target, params.cwd, params.trustedCwd) ?? resolveMutationTarget(target, params.cwd);
2376
+ if (!resolved) {
2377
+ continue;
2378
+ }
2379
+ const relative = relativeWithinRepo(params.repoRoot, resolved);
2380
+ if (relative !== null && matchesSensitivePath(relative.replaceAll("\\", "/"), params.sensitivePaths)) {
2381
+ return {
2382
+ local_recoverable: false,
2383
+ destroys_outside_repo: false,
2384
+ destroys_history_or_secrets: true,
2385
+ reason: "sensitive_path_mutation"
2386
+ };
2387
+ }
2388
+ if (relative === null && isOutsideRepoSecretCredentialPath(resolved)) {
2389
+ return {
2390
+ local_recoverable: false,
2391
+ destroys_outside_repo: false,
2392
+ destroys_history_or_secrets: true,
2393
+ reason: "outside_repo_secret_credential_path"
2394
+ };
2395
+ }
2396
+ if (isPersistentAgentPath(resolved)) {
2397
+ return {
2398
+ local_recoverable: false,
2399
+ destroys_outside_repo: false,
2400
+ destroys_history_or_secrets: true,
2401
+ reason: "persistent_agent_path"
2402
+ };
2403
+ }
2404
+ }
2405
+ return null;
2406
+ }
2407
+ function mutationPrescanRequiresAsk(params) {
2408
+ const prescan = prescanMutationTargets(params);
2409
+ return prescan && tier1RequiresAsk(prescan) ? prescan : null;
2410
+ }
1642
2411
  function createDeterministicJudgeStub() {
1643
2412
  return {
1644
2413
  evaluate() {
1645
2414
  return Promise.resolve({
1646
- external_change: false,
2415
+ local_recoverable: true,
1647
2416
  destroys_outside_repo: false,
1648
2417
  destroys_history_or_secrets: false,
1649
2418
  reason: "deterministic_stub"
@@ -1775,8 +2544,7 @@ function createOpenAiCompatibleJudge(options) {
1775
2544
  };
1776
2545
  return failClosedVerdict("outbound_scrub_failed");
1777
2546
  }
1778
- const { resolveJudgeApiKey: resolveJudgeApiKey2 } = await Promise.resolve().then(() => (init_judge_api_key(), judge_api_key_exports));
1779
- const resolvedKey = resolveJudgeApiKey2();
2547
+ const resolvedKey = options.resolveApiKey ? await options.resolveApiKey() : { key: options.apiKey ?? null, source: null };
1780
2548
  const apiKey = options.apiKey ?? resolvedKey.key;
1781
2549
  if (!apiKey) {
1782
2550
  judge.lastTrace = {
@@ -1840,43 +2608,385 @@ function createOpenAiCompatibleJudge(options) {
1840
2608
  return judge;
1841
2609
  }
1842
2610
  function tier1RequiresAsk(verdict2) {
1843
- return verdict2.external_change || verdict2.destroys_outside_repo || verdict2.destroys_history_or_secrets;
2611
+ return !verdict2.local_recoverable || verdict2.destroys_history_or_secrets;
1844
2612
  }
1845
2613
 
1846
- // src/core/v2/judge-factory.ts
1847
- var FIXTURE_MODELS_URL = new URL("../../../fixtures/judge-models.json", import.meta.url);
1848
- function resolveCloudModel(requested, pinned) {
1849
- if (requested === "auto") {
1850
- const envResolved = process.env.BELAY_JUDGE_MODEL_RESOLVED?.trim();
2614
+ // src/core/verdict/judge-cli.ts
2615
+ import { spawn } from "node:child_process";
2616
+ var CLI_BINARIES = {
2617
+ codex: "codex",
2618
+ cursor: "cursor-agent",
2619
+ claude: "claude"
2620
+ };
2621
+ function failClosedVerdict2(reason) {
2622
+ return {
2623
+ local_recoverable: false,
2624
+ destroys_outside_repo: true,
2625
+ destroys_history_or_secrets: true,
2626
+ external_change: true,
2627
+ reason
2628
+ };
2629
+ }
2630
+ function transportForProvider(providerId) {
2631
+ return `${providerId}-cli`;
2632
+ }
2633
+ function traceProvider(providerId) {
2634
+ if (providerId === "claude") {
2635
+ return "anthropic";
2636
+ }
2637
+ return "openai-compatible";
2638
+ }
2639
+ function initialTrace(options) {
2640
+ return {
2641
+ provider: traceProvider(options.providerId),
2642
+ modelRequested: options.modelRequested,
2643
+ modelResolved: options.modelResolved,
2644
+ latencyMs: 0,
2645
+ transport: transportForProvider(options.providerId)
2646
+ };
2647
+ }
2648
+ function buildCliInvocation(providerId, prompt, model) {
2649
+ if (providerId === "codex") {
1851
2650
  return {
1852
- requested,
1853
- resolved: envResolved || pinned.autoResolved
2651
+ binary: CLI_BINARIES[providerId],
2652
+ args: [
2653
+ "exec",
2654
+ "--json",
2655
+ "--skip-git-repo-check",
2656
+ "--sandbox",
2657
+ "read-only",
2658
+ "--model",
2659
+ model,
2660
+ "-"
2661
+ ],
2662
+ stdin: prompt
1854
2663
  };
1855
2664
  }
1856
- return { requested, resolved: requested };
2665
+ if (providerId === "cursor") {
2666
+ return {
2667
+ binary: CLI_BINARIES[providerId],
2668
+ args: [
2669
+ "--print",
2670
+ "--output-format",
2671
+ "json",
2672
+ "--mode",
2673
+ "ask",
2674
+ "--sandbox",
2675
+ "enabled",
2676
+ "--trust",
2677
+ "--model",
2678
+ model,
2679
+ prompt
2680
+ ]
2681
+ };
2682
+ }
2683
+ return {
2684
+ binary: CLI_BINARIES[providerId],
2685
+ args: [
2686
+ "-p",
2687
+ "--output-format",
2688
+ "json",
2689
+ "--permission-mode",
2690
+ "plan",
2691
+ "--tools",
2692
+ "",
2693
+ "--bare",
2694
+ "--model",
2695
+ model
2696
+ ],
2697
+ stdin: prompt
2698
+ };
2699
+ }
2700
+ function tryParseJson(raw) {
2701
+ try {
2702
+ return JSON.parse(raw);
2703
+ } catch {
2704
+ return null;
2705
+ }
2706
+ }
2707
+ function extractVerdictFromJsonValue(value) {
2708
+ if (typeof value === "string") {
2709
+ return parseTier1Json(value.trim());
2710
+ }
2711
+ if (Array.isArray(value)) {
2712
+ for (let index = value.length - 1; index >= 0; index -= 1) {
2713
+ const verdict2 = extractVerdictFromJsonValue(value[index]);
2714
+ if (verdict2) {
2715
+ return verdict2;
2716
+ }
2717
+ }
2718
+ return null;
2719
+ }
2720
+ if (!value || typeof value !== "object") {
2721
+ return null;
2722
+ }
2723
+ const record = value;
2724
+ for (const key of ["result", "content", "text", "message", "output_text", "final"]) {
2725
+ if (!(key in record)) {
2726
+ continue;
2727
+ }
2728
+ const verdict2 = extractVerdictFromJsonValue(record[key]);
2729
+ if (verdict2) {
2730
+ return verdict2;
2731
+ }
2732
+ }
2733
+ for (const candidate of Object.values(record).reverse()) {
2734
+ const verdict2 = extractVerdictFromJsonValue(candidate);
2735
+ if (verdict2) {
2736
+ return verdict2;
2737
+ }
2738
+ }
2739
+ return null;
2740
+ }
2741
+ function parseCliJsonLines(raw) {
2742
+ const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
2743
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
2744
+ const parsed = tryParseJson(lines[index] ?? "");
2745
+ if (!parsed) {
2746
+ continue;
2747
+ }
2748
+ const verdict2 = extractVerdictFromJsonValue(parsed);
2749
+ if (verdict2) {
2750
+ return verdict2;
2751
+ }
2752
+ }
2753
+ return null;
2754
+ }
2755
+ function parseCliJudgeOutput(providerId, raw) {
2756
+ const direct = parseTier1Json(raw);
2757
+ if (direct) {
2758
+ return direct;
2759
+ }
2760
+ const parsed = tryParseJson(raw);
2761
+ if (parsed) {
2762
+ const envelopeVerdict = extractVerdictFromJsonValue(parsed);
2763
+ if (envelopeVerdict) {
2764
+ return envelopeVerdict;
2765
+ }
2766
+ }
2767
+ if (providerId === "codex") {
2768
+ return parseCliJsonLines(raw);
2769
+ }
2770
+ return parseCliJsonLines(raw);
2771
+ }
2772
+ async function runCliJson(invocation, timeoutMs) {
2773
+ const { binary, args, stdin } = invocation;
2774
+ return new Promise((resolve, reject) => {
2775
+ const child = spawn(binary, args, {
2776
+ stdio: ["pipe", "pipe", "pipe"]
2777
+ });
2778
+ let stdout = "";
2779
+ let stderr = "";
2780
+ const timer = setTimeout(() => {
2781
+ child.kill("SIGTERM");
2782
+ reject(new Error(`${binary} timed out`));
2783
+ }, timeoutMs);
2784
+ child.stdout.on("data", (chunk) => {
2785
+ stdout += String(chunk);
2786
+ });
2787
+ child.stderr.on("data", (chunk) => {
2788
+ stderr += String(chunk);
2789
+ });
2790
+ if (stdin !== void 0) {
2791
+ child.stdin.write(stdin);
2792
+ }
2793
+ child.stdin.end();
2794
+ child.on("error", (error) => {
2795
+ clearTimeout(timer);
2796
+ reject(error);
2797
+ });
2798
+ child.on("close", (code) => {
2799
+ clearTimeout(timer);
2800
+ if (code === 0 && stdout.trim()) {
2801
+ resolve(stdout.trim());
2802
+ return;
2803
+ }
2804
+ reject(new Error(stderr.trim() || `${binary} exited with code ${code ?? "unknown"}`));
2805
+ });
2806
+ });
2807
+ }
2808
+ function createCliJudge(options) {
2809
+ const transport = transportForProvider(options.providerId);
2810
+ const timeoutMs = options.timeoutMs ?? 25e3;
2811
+ const providerId = options.providerId;
2812
+ const runCommand = options.runCliCommand ?? runCliJson;
2813
+ const judge = {
2814
+ lastTrace: initialTrace(options),
2815
+ async evaluate(input) {
2816
+ const started = Date.now();
2817
+ const prescan = input.innerCode ? prescanInterpreterCode(input.innerCode) : null;
2818
+ if (prescan?.destroys_history_or_secrets) {
2819
+ judge.lastTrace = {
2820
+ provider: traceProvider(options.providerId),
2821
+ modelRequested: options.modelRequested,
2822
+ modelResolved: options.modelResolved,
2823
+ latencyMs: Date.now() - started,
2824
+ transport
2825
+ };
2826
+ return prescan;
2827
+ }
2828
+ const scrubbed = scrubOutboundForJudge(input.text, {
2829
+ sensitivePaths: options.sensitivePaths,
2830
+ scrubOptions: options.scrubOptions
2831
+ });
2832
+ if (!scrubbed.ok) {
2833
+ judge.lastTrace = {
2834
+ provider: "fallback",
2835
+ modelRequested: options.modelRequested,
2836
+ modelResolved: options.modelResolved,
2837
+ latencyMs: Date.now() - started,
2838
+ fallbackReason: scrubbed.reason
2839
+ };
2840
+ return failClosedVerdict2("outbound_scrub_failed");
2841
+ }
2842
+ const prompt = buildTier1Prompt(scrubbed.text);
2843
+ try {
2844
+ const raw = await runCommand(
2845
+ buildCliInvocation(providerId, prompt, options.modelResolved),
2846
+ timeoutMs
2847
+ );
2848
+ const parsed = parseCliJudgeOutput(providerId, raw);
2849
+ if (!parsed) {
2850
+ judge.lastTrace = {
2851
+ provider: "fallback",
2852
+ modelRequested: options.modelRequested,
2853
+ modelResolved: options.modelResolved,
2854
+ latencyMs: Date.now() - started,
2855
+ fallbackReason: `${providerId}_cli_parse_error`
2856
+ };
2857
+ return failClosedVerdict2(`${providerId}_cli_parse_error`);
2858
+ }
2859
+ judge.lastTrace = {
2860
+ provider: traceProvider(options.providerId),
2861
+ modelRequested: options.modelRequested,
2862
+ modelResolved: options.modelResolved,
2863
+ latencyMs: Date.now() - started,
2864
+ transport,
2865
+ outboundRedacted: true
2866
+ };
2867
+ return parsed;
2868
+ } catch {
2869
+ judge.lastTrace = {
2870
+ provider: "fallback",
2871
+ modelRequested: options.modelRequested,
2872
+ modelResolved: options.modelResolved,
2873
+ latencyMs: Date.now() - started,
2874
+ fallbackReason: `${providerId}_cli_unavailable`
2875
+ };
2876
+ return failClosedVerdict2(`${providerId}_cli_unavailable`);
2877
+ }
2878
+ }
2879
+ };
2880
+ return judge;
2881
+ }
2882
+ function createCodexCliJudge(options) {
2883
+ return createCliJudge({ ...options, providerId: "codex" });
2884
+ }
2885
+ function createCursorCliJudge(options) {
2886
+ return createCliJudge({ ...options, providerId: "cursor" });
2887
+ }
2888
+ function createClaudeCliJudge(options) {
2889
+ return createCliJudge({ ...options, providerId: "claude" });
2890
+ }
2891
+
2892
+ // src/core/verdict/judge-factory.ts
2893
+ var FIXTURE_MODELS_URL = new URL("../../../fixtures/judge-models.json", import.meta.url);
2894
+ function resolveJudgeModel(judge) {
2895
+ rejectDeprecatedJudgeModelAuto(judge.model);
2896
+ const requested = judge.model;
2897
+ const envOverride = testJudgeModelOverride();
2898
+ return { requested, resolved: envOverride || requested };
2899
+ }
2900
+ function testJudgeModelOverride() {
2901
+ if (!process.env.VITEST && !process.env.VITEST_WORKER_ID) {
2902
+ return void 0;
2903
+ }
2904
+ return process.env.BELAY_JUDGE_MODEL_RESOLVED?.trim() || void 0;
2905
+ }
2906
+ function providerIdFromJudge(judge) {
2907
+ if (judge.providerId) {
2908
+ const normalized = normalizeLegacyProviderId(judge.providerId);
2909
+ if (normalized) {
2910
+ return normalized;
2911
+ }
2912
+ }
2913
+ return inferProviderIdFromConfig(judge);
2914
+ }
2915
+ function createCliJudgeForProvider(providerId, judgeConfig, config) {
2916
+ const { resolved } = resolveJudgeModel(judgeConfig);
2917
+ const options = {
2918
+ modelRequested: judgeConfig.model,
2919
+ modelResolved: resolved,
2920
+ timeoutMs: judgeConfig.timeoutMs,
2921
+ sensitivePaths: config.classifier.sensitivePaths,
2922
+ scrubOptions: scrubOptionsFromConfig(config)
2923
+ };
2924
+ if (providerId === "codex") {
2925
+ return createCodexCliJudge(options);
2926
+ }
2927
+ if (providerId === "cursor") {
2928
+ return createCursorCliJudge(options);
2929
+ }
2930
+ return createClaudeCliJudge(options);
1857
2931
  }
1858
2932
  function createJudgeFromConfig(config, options = {}) {
2933
+ if (process.env.BELAY_DETERMINISTIC_JUDGE === "1") {
2934
+ return createDeterministicJudgeStub();
2935
+ }
1859
2936
  const judgeConfig = config.judge;
2937
+ if (judgeConfig.providerId && isRemovedProviderId(String(judgeConfig.providerId))) {
2938
+ const { resolved: resolved2 } = resolveJudgeModel(judgeConfig);
2939
+ return createFailClosedJudge({
2940
+ reason: "judge_provider_removed",
2941
+ fallbackReason: "provider_migration_required",
2942
+ modelRequested: judgeConfig.model,
2943
+ modelResolved: resolved2
2944
+ });
2945
+ }
1860
2946
  const provider = normalizeJudgeProvider(judgeConfig.provider);
2947
+ const providerId = providerIdFromJudge(judgeConfig);
2948
+ const catalogSpec = getJudgeProviderSpec(providerId);
2949
+ const { resolved } = resolveJudgeModel(judgeConfig);
2950
+ const runtime = detectJudgeRuntimeCapabilities(providerId);
1861
2951
  if (provider === "openai-compatible") {
1862
2952
  const endpoint = judgeConfig.endpoint?.trim();
2953
+ if (!endpoint && runtime.cliTransport) {
2954
+ if (providerId === "codex" || providerId === "cursor") {
2955
+ return createCliJudgeForProvider(providerId, judgeConfig, config);
2956
+ }
2957
+ }
1863
2958
  if (!endpoint) {
1864
2959
  return createFailClosedJudge({
1865
2960
  reason: "openai_compatible_endpoint_missing",
1866
2961
  fallbackReason: "missing_endpoint",
1867
2962
  modelRequested: judgeConfig.model,
1868
- modelResolved: judgeConfig.model
2963
+ modelResolved: resolved
1869
2964
  });
1870
2965
  }
1871
- const pinned = options.pinnedModels ?? { autoResolved: "composer-2.5" };
1872
- const { resolved } = resolveCloudModel(judgeConfig.model, pinned);
2966
+ if (isCloudJudgeConfig(judgeConfig) && !hasValidCloudConsent(judgeConfig)) {
2967
+ return createFailClosedJudge({
2968
+ reason: "openai_compatible_consent_missing",
2969
+ fallbackReason: "cloud_consent_missing",
2970
+ modelRequested: judgeConfig.model,
2971
+ modelResolved: resolved
2972
+ });
2973
+ }
2974
+ const repoRoot = options.repoRoot ?? process.cwd();
2975
+ const repoLocalStateDir = repoLocalStateDirFor(repoRoot, config);
1873
2976
  return createOpenAiCompatibleJudge({
1874
2977
  endpoint,
1875
2978
  modelRequested: judgeConfig.model,
1876
2979
  modelResolved: resolved,
1877
2980
  timeoutMs: judgeConfig.timeoutMs,
1878
2981
  sensitivePaths: config.classifier.sensitivePaths,
1879
- scrubOptions: scrubOptionsFromConfig(config)
2982
+ scrubOptions: scrubOptionsFromConfig(config),
2983
+ resolveApiKey: async () => resolveJudgeCredential({
2984
+ judge: judgeConfig,
2985
+ catalogSpec: catalogSpec ?? void 0,
2986
+ repoRoot,
2987
+ repoLocalStateDir,
2988
+ config
2989
+ })
1880
2990
  });
1881
2991
  }
1882
2992
  if (provider === "ollama") {
@@ -1887,105 +2997,32 @@ function createJudgeFromConfig(config, options = {}) {
1887
2997
  keepAlive: judgeConfig.keepAlive
1888
2998
  });
1889
2999
  }
1890
- return createDeterministicJudgeStub();
1891
- }
1892
-
1893
- // src/core/v2/verdict.ts
1894
- import path11 from "node:path";
1895
-
1896
- // src/core/v2/containment.ts
1897
- import path8 from "node:path";
1898
- function expandHome(token) {
1899
- if (token === "~" || token.startsWith("~/")) {
1900
- const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
1901
- if (!home) {
1902
- return token;
1903
- }
1904
- return token === "~" ? home : path8.join(home, token.slice(2));
1905
- }
1906
- return token;
1907
- }
1908
- function resolveTrustedPath(token, trustedCwd, trusted) {
1909
- if (!token || token === "--" || token.startsWith("-")) {
1910
- return null;
1911
- }
1912
- if (!trusted || !trustedCwd) {
1913
- return null;
1914
- }
1915
- const expanded = expandHome(token);
1916
- if (path8.isAbsolute(expanded)) {
1917
- return canonicalPath(expanded);
1918
- }
1919
- return canonicalPath(path8.resolve(trustedCwd, expanded));
1920
- }
1921
- function locationForPath(resolvedPath, repoRoot) {
1922
- if (!resolvedPath) {
1923
- return "unknown";
1924
- }
1925
- if (pathWithinRoot(repoRoot, resolvedPath)) {
1926
- return "repo_local";
1927
- }
1928
- return "repo_outside";
1929
- }
1930
- function isGitPath(resolvedPath, repoRoot) {
1931
- const relative = relativeWithinRepo(repoRoot, resolvedPath);
1932
- if (!relative) {
1933
- return false;
1934
- }
1935
- const normalized = relative.replaceAll("\\", "/");
1936
- return normalized === ".git" || normalized.startsWith(".git/");
1937
- }
1938
- function isHighStakesPath(resolvedPath, repoRoot, sensitivePaths, protectedRoots = []) {
1939
- if (isGitPath(resolvedPath, repoRoot)) {
1940
- return true;
1941
- }
1942
- const relative = relativeWithinRepo(repoRoot, resolvedPath);
1943
- const checkPath = relative ?? resolvedPath;
1944
- if (matchesSensitivePath(checkPath.replaceAll("\\", "/"), sensitivePaths)) {
1945
- return true;
1946
- }
1947
- return protectedRoots.some((root) => pathWithinRoot(root, resolvedPath));
1948
- }
1949
- function analyzePathTargets(params) {
1950
- const signals = [];
1951
- if (!params.trustedCwd || !params.cwd) {
1952
- return {
1953
- location: "unknown",
1954
- isHighStakes: false,
1955
- signals: ["missing_trusted_cwd"]
1956
- };
1957
- }
1958
- const locations = /* @__PURE__ */ new Set();
1959
- let isHighStakes = false;
1960
- for (const target of params.targets) {
1961
- const resolved = resolveTrustedPath(target, params.cwd, params.trustedCwd) ?? resolveMutationTarget(target, params.cwd);
1962
- const location2 = locationForPath(resolved, params.repoRoot);
1963
- locations.add(location2);
1964
- if (resolved && isHighStakesPath(
1965
- resolved,
1966
- params.repoRoot,
1967
- params.sensitivePaths,
1968
- params.protectedArtifactRoots
1969
- )) {
1970
- isHighStakes = true;
1971
- signals.push("high_stakes_path");
3000
+ if (provider === "anthropic") {
3001
+ if (runtime.cliTransport) {
3002
+ return createClaudeCliJudge({
3003
+ modelRequested: judgeConfig.model,
3004
+ modelResolved: resolved,
3005
+ timeoutMs: judgeConfig.timeoutMs,
3006
+ sensitivePaths: config.classifier.sensitivePaths,
3007
+ scrubOptions: scrubOptionsFromConfig(config)
3008
+ });
1972
3009
  }
3010
+ return createFailClosedJudge({
3011
+ reason: "anthropic_not_implemented",
3012
+ fallbackReason: "anthropic_runtime_unavailable",
3013
+ modelRequested: judgeConfig.model,
3014
+ modelResolved: resolved
3015
+ });
1973
3016
  }
1974
- let location = "unknown";
1975
- if (locations.size === 0) {
1976
- location = "unknown";
1977
- } else if (locations.size === 1) {
1978
- location = [...locations][0] ?? "unknown";
1979
- } else {
1980
- location = "mixed";
1981
- }
1982
- return { location, isHighStakes, signals };
1983
- }
1984
- function cwdRelative(repoRoot, cwd) {
1985
- return relativeWithinRepo(repoRoot, cwd) ?? cwd;
3017
+ return createFailClosedJudge({
3018
+ reason: "unsupported_judge_provider",
3019
+ fallbackReason: "unsupported_provider",
3020
+ modelRequested: judgeConfig.model,
3021
+ modelResolved: resolved
3022
+ });
1986
3023
  }
1987
3024
 
1988
- // src/core/v2/egress-classify.ts
3025
+ // src/core/verdict/egress-classify.ts
1989
3026
  var EGRESS_TOOL_HEADS = /* @__PURE__ */ new Set([
1990
3027
  "aws",
1991
3028
  "curl",
@@ -2083,6 +3120,9 @@ function classifyAws(tokens) {
2083
3120
  if (/\bs3\s+rm\b/.test(joined)) {
2084
3121
  return "destructive";
2085
3122
  }
3123
+ if (/\bs3\s+mb\b/.test(joined)) {
3124
+ return "destructive";
3125
+ }
2086
3126
  if (/\bs3\s+sync\b/.test(joined)) {
2087
3127
  return "destructive";
2088
3128
  }
@@ -2193,17 +3233,61 @@ function classifyNetlify(tokens) {
2193
3233
  return "ambiguous";
2194
3234
  }
2195
3235
 
2196
- // src/core/v2/fingerprint.ts
3236
+ // src/core/verdict/fingerprint.ts
2197
3237
  function verdictFingerprint(cwdRelative2, commandRedacted) {
2198
3238
  return hashValue(`v2:${cwdRelative2}:${commandRedacted}`);
2199
3239
  }
2200
3240
 
2201
- // src/core/v2/launcher-resolve.ts
3241
+ // src/core/verdict/launcher-resolve.ts
2202
3242
  import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
2203
- import path9 from "node:path";
3243
+ import path14 from "node:path";
2204
3244
  var MAX_RESOLVE_DEPTH = 8;
3245
+ var PNPM_BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
3246
+ "add",
3247
+ "audit",
3248
+ "cache",
3249
+ "config",
3250
+ "deploy",
3251
+ "dlx",
3252
+ "exec",
3253
+ "fetch",
3254
+ "help",
3255
+ "import",
3256
+ "init",
3257
+ "install",
3258
+ "i",
3259
+ "licenses",
3260
+ "link",
3261
+ "list",
3262
+ "outdated",
3263
+ "pack",
3264
+ "patch",
3265
+ "patch-commit",
3266
+ "patch-remove",
3267
+ "publish",
3268
+ "prune",
3269
+ "rebuild",
3270
+ "remove",
3271
+ "rm",
3272
+ "store",
3273
+ "unlink",
3274
+ "update",
3275
+ "up",
3276
+ "why"
3277
+ ]);
3278
+ var PNPM_EXEC_LIKE_HEADS = /* @__PURE__ */ new Set([
3279
+ "vitest",
3280
+ "vite",
3281
+ "biome",
3282
+ "eslint",
3283
+ "jest",
3284
+ "mocha",
3285
+ "tsc",
3286
+ "tsx",
3287
+ "node"
3288
+ ]);
2205
3289
  function readPackageJson(dir) {
2206
- const packagePath = path9.join(dir, "package.json");
3290
+ const packagePath = path14.join(dir, "package.json");
2207
3291
  if (!existsSync4(packagePath)) {
2208
3292
  return null;
2209
3293
  }
@@ -2214,17 +3298,17 @@ function readPackageJson(dir) {
2214
3298
  }
2215
3299
  }
2216
3300
  function findPackageJson(startDir, stopDir) {
2217
- let current = path9.resolve(startDir);
2218
- const stop = path9.resolve(stopDir);
3301
+ let current = path14.resolve(startDir);
3302
+ const stop = path14.resolve(stopDir);
2219
3303
  while (true) {
2220
- const packagePath = path9.join(current, "package.json");
3304
+ const packagePath = path14.join(current, "package.json");
2221
3305
  if (existsSync4(packagePath)) {
2222
3306
  return packagePath;
2223
3307
  }
2224
- if (current === stop || current === path9.dirname(current)) {
3308
+ if (current === stop || current === path14.dirname(current)) {
2225
3309
  return existsSync4(packagePath) ? packagePath : null;
2226
3310
  }
2227
- const parent = path9.dirname(current);
3311
+ const parent = path14.dirname(current);
2228
3312
  if (!parent.startsWith(stop) && parent !== current) {
2229
3313
  }
2230
3314
  if (parent === current) {
@@ -2256,6 +3340,12 @@ function npmScriptName(tokens) {
2256
3340
  if (launcher[0] === "pnpm" && launcher[1] === "run" && launcher[2]) {
2257
3341
  return launcher[2];
2258
3342
  }
3343
+ if (launcher[0] === "pnpm" && launcher[1] === "test") {
3344
+ return "test";
3345
+ }
3346
+ if (launcher[0] === "pnpm" && launcher[1] && !launcher[1].startsWith("-") && !PNPM_BUILTIN_COMMANDS.has(launcher[1])) {
3347
+ return launcher[1];
3348
+ }
2259
3349
  if (launcher[0] === "npm" && launcher[1] && launcher[1] !== "run" && launcher[1] !== "install") {
2260
3350
  return null;
2261
3351
  }
@@ -2275,7 +3365,7 @@ function resolveNpmRecipe(cwd, repoRoot, scriptName, extraArgs) {
2275
3365
  }
2276
3366
  return { recipes: [], opaque: true, reason: "package_json_missing" };
2277
3367
  }
2278
- const pkg = readPackageJson(path9.dirname(packagePath));
3368
+ const pkg = readPackageJson(path14.dirname(packagePath));
2279
3369
  const scripts = pkg?.scripts;
2280
3370
  if (!scripts || typeof scripts !== "object") {
2281
3371
  return { recipes: [], opaque: true, reason: "package_scripts_missing" };
@@ -2340,20 +3430,20 @@ function parseMakefileRecipes(makefilePath) {
2340
3430
  function resolveMakeRecipe(cwd, repoRoot, target) {
2341
3431
  const candidates = ["Makefile", "makefile", "GNUmakefile"];
2342
3432
  let makefilePath = null;
2343
- let searchDir = path9.resolve(cwd);
2344
- const stop = path9.resolve(repoRoot);
3433
+ let searchDir = path14.resolve(cwd);
3434
+ const stop = path14.resolve(repoRoot);
2345
3435
  while (true) {
2346
3436
  for (const name of candidates) {
2347
- const candidate = path9.join(searchDir, name);
3437
+ const candidate = path14.join(searchDir, name);
2348
3438
  if (existsSync4(candidate)) {
2349
3439
  makefilePath = candidate;
2350
3440
  break;
2351
3441
  }
2352
3442
  }
2353
- if (makefilePath || searchDir === stop || searchDir === path9.dirname(searchDir)) {
3443
+ if (makefilePath || searchDir === stop || searchDir === path14.dirname(searchDir)) {
2354
3444
  break;
2355
3445
  }
2356
- searchDir = path9.dirname(searchDir);
3446
+ searchDir = path14.dirname(searchDir);
2357
3447
  }
2358
3448
  if (!makefilePath) {
2359
3449
  return { recipes: [], opaque: true, reason: "unknown_local_effect" };
@@ -2377,11 +3467,31 @@ function resolveLauncherRecipe(params) {
2377
3467
  const tokens = params.tokens;
2378
3468
  const scriptName = npmScriptName(tokens);
2379
3469
  if (scriptName) {
2380
- return resolveNpmRecipe(params.cwd, params.repoRoot, scriptName, forwardedArgs(tokens));
3470
+ const resolution = resolveNpmRecipe(
3471
+ params.cwd,
3472
+ params.repoRoot,
3473
+ scriptName,
3474
+ forwardedArgs(tokens)
3475
+ );
3476
+ if (tokens[0] === "pnpm" && tokens[1] && PNPM_EXEC_LIKE_HEADS.has(tokens[1]) && resolution.reason === "npm_script_undefined") {
3477
+ return {
3478
+ recipes: [tokens.slice(1).join(" ")],
3479
+ opaque: false,
3480
+ reason: "pnpm_exec_like"
3481
+ };
3482
+ }
3483
+ return resolution;
2381
3484
  }
2382
3485
  if (tokens[0] === "make" && tokens[1] && !tokens[1].startsWith("-")) {
2383
3486
  return resolveMakeRecipe(params.cwd, params.repoRoot, tokens[1]);
2384
3487
  }
3488
+ if (tokens[0] === "pnpm" && tokens[1] && PNPM_EXEC_LIKE_HEADS.has(tokens[1])) {
3489
+ return {
3490
+ recipes: [tokens.slice(1).join(" ")],
3491
+ opaque: false,
3492
+ reason: "pnpm_exec_like"
3493
+ };
3494
+ }
2385
3495
  return null;
2386
3496
  }
2387
3497
  function isRoutineLauncher(tokens) {
@@ -2397,7 +3507,7 @@ function matchesCustomCommand(normalizedCommand, key, pattern) {
2397
3507
  return normalizedCommand === trimmed || key === trimmed;
2398
3508
  }
2399
3509
 
2400
- // src/core/v2/overrides.ts
3510
+ // src/core/verdict/overrides.ts
2401
3511
  function matchesCustomPatterns(command, segment, patterns) {
2402
3512
  if (!patterns || patterns.length === 0) {
2403
3513
  return false;
@@ -2436,8 +3546,8 @@ function askFromCustomExternal(opacity) {
2436
3546
  };
2437
3547
  }
2438
3548
 
2439
- // src/core/v2/parser.ts
2440
- import path10 from "node:path";
3549
+ // src/core/verdict/parser.ts
3550
+ import path15 from "node:path";
2441
3551
 
2442
3552
  // src/core/shell-substitution.ts
2443
3553
  function findCommandSubstitutions(command) {
@@ -2660,7 +3770,7 @@ function hasUnbalancedDollarParen(command) {
2660
3770
  return depth > 0;
2661
3771
  }
2662
3772
 
2663
- // src/core/v2/parser.ts
3773
+ // src/core/verdict/parser.ts
2664
3774
  var ENV_PREFIX_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=(?:'[^']*'|"[^"]*"|\S+)$/;
2665
3775
  var TRANSPARENT_WRAPPERS = /* @__PURE__ */ new Set([
2666
3776
  "sudo",
@@ -2687,7 +3797,7 @@ var INTERPRETER_SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
2687
3797
  ".sh"
2688
3798
  ]);
2689
3799
  function normalizeHead(token) {
2690
- const base = path10.basename(token);
3800
+ const base = path15.basename(token);
2691
3801
  if (base && base !== "." && base !== "..") {
2692
3802
  return base;
2693
3803
  }
@@ -2792,7 +3902,7 @@ function isBareInterpreter(tokens) {
2792
3902
  return false;
2793
3903
  }
2794
3904
  const scriptArg = args.find((token) => !token.startsWith("-"));
2795
- if (scriptArg && INTERPRETER_SCRIPT_EXTENSIONS.has(path10.extname(scriptArg))) {
3905
+ if (scriptArg && INTERPRETER_SCRIPT_EXTENSIONS.has(path15.extname(scriptArg))) {
2796
3906
  return false;
2797
3907
  }
2798
3908
  if (scriptArg) {
@@ -2860,7 +3970,7 @@ function redactCommand(command) {
2860
3970
  return command.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]").replace(/sk-[A-Za-z0-9]{8,}/g, "sk-[REDACTED]").trim();
2861
3971
  }
2862
3972
 
2863
- // src/core/v2/verdict.ts
3973
+ // src/core/verdict/verdict.ts
2864
3974
  var DEFAULT_MAX_DEPTH = 8;
2865
3975
  var TIER0_EXTERNAL_KEYS = /* @__PURE__ */ new Set([
2866
3976
  "git push",
@@ -2943,6 +4053,7 @@ var LOCAL_ROUTINE_HEADS = /* @__PURE__ */ new Set([
2943
4053
  "make",
2944
4054
  "cmake"
2945
4055
  ]);
4056
+ var BELAY_SELF_COMMANDS = /* @__PURE__ */ new Set(["approve", "revoke"]);
2946
4057
  var FIND_DANGEROUS_FLAGS = /* @__PURE__ */ new Set(["-delete", "-exec", "-execdir", "-ok", "-okdir"]);
2947
4058
  function isFindDangerous(tokens) {
2948
4059
  return tokens.some(
@@ -3090,8 +4201,36 @@ function tier0ExternalMatch(key, head, tokens) {
3090
4201
  }
3091
4202
  return false;
3092
4203
  }
3093
- function tier0HighStakesRm(tokens, context) {
4204
+ function isBelayJudgeSelfCommand(tokens) {
4205
+ return tokens[0] === "belay" && tokens[1] === "judge" && (tokens[2] === "use" || tokens[2] === "status" || tokens[2] === "list" || tokens[2] === "test" || tokens[2] === "consent");
4206
+ }
4207
+ function isBelayConfigSelfCommand(tokens) {
4208
+ if (tokens[0] !== "belay" || tokens[1] !== "config") {
4209
+ return false;
4210
+ }
4211
+ const sub = tokens[2];
4212
+ if (!sub) {
4213
+ return true;
4214
+ }
4215
+ if (sub === "list" || sub === "get" || sub === "judge") {
4216
+ return true;
4217
+ }
4218
+ if (sub === "credential") {
4219
+ return true;
4220
+ }
4221
+ if (sub === "set" || sub === "unset") {
4222
+ const pathKey = tokens[3] ?? "";
4223
+ return pathKey.startsWith("judge.");
4224
+ }
4225
+ return false;
4226
+ }
4227
+ function isBelaySelfCommand(tokens) {
3094
4228
  const head = tokens[0] ?? "";
4229
+ const subcommand = tokens[1] ?? "";
4230
+ return head === "belay" && (BELAY_SELF_COMMANDS.has(subcommand) || isBelayJudgeSelfCommand(tokens) || isBelayConfigSelfCommand(tokens));
4231
+ }
4232
+ function tier0HighStakesRm(tokens, context) {
4233
+ const head = (tokens[0] ?? "").split("/").pop() ?? "";
3095
4234
  if (head !== "rm") {
3096
4235
  return null;
3097
4236
  }
@@ -3114,34 +4253,44 @@ function tier0HighStakesRm(tokens, context) {
3114
4253
  signals: ["missing_trusted_cwd", ...analysis.signals]
3115
4254
  });
3116
4255
  }
3117
- if (analysis.isHighStakes) {
3118
- return askVerdict({
3119
- location: analysis.location,
3120
- opacity: "transparent",
3121
- effect: "local_mutation",
3122
- confidence: "deterministic",
3123
- reason: "high_stakes_path",
3124
- signals: ["high_stakes_path", ...analysis.signals]
3125
- });
3126
- }
3127
4256
  for (const target of targets) {
3128
- if (target === "~" || target.startsWith("~/") || target.startsWith("/")) {
3129
- const resolved = path11.resolve(
3130
- target === "~" || target.startsWith("~/") ? process.env.HOME ?? "/" : context.cwd,
3131
- target
3132
- );
3133
- const relative = relativeWithinRepo(context.repoRoot, resolved);
3134
- if (relative === null) {
3135
- return askVerdict({
3136
- location: "repo_outside",
3137
- opacity: "transparent",
3138
- effect: "local_mutation",
3139
- confidence: "deterministic",
3140
- reason: "repo_outside_mutation",
3141
- signals: ["repo_outside_mutation"]
3142
- });
4257
+ const normalized = target.replace(/^['"]|['"]$/g, "");
4258
+ if (normalized === "~" || normalized === "$HOME" || normalized.startsWith("~/")) {
4259
+ return askVerdict({
4260
+ location: "repo_outside",
4261
+ opacity: "transparent",
4262
+ effect: "local_mutation",
4263
+ confidence: "deterministic",
4264
+ reason: "high_stakes_path",
4265
+ signals: ["catastrophic_home_deletion", ...analysis.signals]
4266
+ });
4267
+ }
4268
+ }
4269
+ if (analysis.isHighStakes && analysis.signals.includes("high_stakes_path")) {
4270
+ let destructiveHighStakes = false;
4271
+ for (const target of targets) {
4272
+ const resolved = resolveTrustedPath(target, context.cwd, context.trustedCwd) ?? resolveMutationTarget(target, context.cwd);
4273
+ if (resolved && isDestructiveHighStakesMutation(
4274
+ head,
4275
+ resolved,
4276
+ context.repoRoot,
4277
+ context.sensitivePaths,
4278
+ context.protectedArtifactRoots
4279
+ )) {
4280
+ destructiveHighStakes = true;
4281
+ break;
3143
4282
  }
3144
4283
  }
4284
+ if (destructiveHighStakes) {
4285
+ return askVerdict({
4286
+ location: analysis.location,
4287
+ opacity: "transparent",
4288
+ effect: "local_mutation",
4289
+ confidence: "deterministic",
4290
+ reason: "high_stakes_path",
4291
+ signals: ["high_stakes_path", ...analysis.signals]
4292
+ });
4293
+ }
3145
4294
  }
3146
4295
  return null;
3147
4296
  }
@@ -3321,6 +4470,16 @@ async function evaluateSegment(command, context, depth) {
3321
4470
  if (rmVerdict) {
3322
4471
  return rmVerdict;
3323
4472
  }
4473
+ if (isBelaySelfCommand(peeled)) {
4474
+ return allowVerdict({
4475
+ location: "unknown",
4476
+ opacity: "transparent",
4477
+ effect: "local_mutation",
4478
+ confidence: "deterministic",
4479
+ reason: "belay_control_plane_command",
4480
+ signals: ["belay_control_plane_command", segment.head]
4481
+ });
4482
+ }
3324
4483
  let effect = "unknown";
3325
4484
  if (READ_ONLY_KEYS.has(segment.key) || READ_ONLY_KEYS.has(segment.head)) {
3326
4485
  effect = "read_only";
@@ -3330,6 +4489,9 @@ async function evaluateSegment(command, context, depth) {
3330
4489
  effect = "local_mutation";
3331
4490
  }
3332
4491
  const pathArgs = extractPathArgs(peeled);
4492
+ if (extractRedirectTargets(peeled).length > 0 && effect === "read_only") {
4493
+ effect = "local_mutation";
4494
+ }
3333
4495
  const pathAnalysis = analyzePathTargets({
3334
4496
  targets: pathArgs,
3335
4497
  cwd: context.cwd,
@@ -3360,15 +4522,46 @@ async function evaluateSegment(command, context, depth) {
3360
4522
  });
3361
4523
  }
3362
4524
  }
3363
- if (pathAnalysis.isHighStakes) {
3364
- return askVerdict({
3365
- location: pathAnalysis.location,
3366
- opacity: "transparent",
3367
- effect: "local_mutation",
3368
- confidence: "deterministic",
3369
- reason: "high_stakes_path",
3370
- signals: pathAnalysis.signals
3371
- });
4525
+ if ((effect === "local_mutation" || effect === "unknown") && context.protectedArtifactRoots && context.protectedArtifactRoots.length > 0) {
4526
+ for (const target of pathArgs) {
4527
+ const resolved = resolveTrustedPath(target, context.cwd, context.trustedCwd) ?? resolveMutationTarget(target, context.cwd);
4528
+ if (resolved && touchesProtectedRoot(resolved, context.protectedArtifactRoots)) {
4529
+ return askVerdict({
4530
+ location: pathAnalysis.location,
4531
+ opacity: "transparent",
4532
+ effect: "local_mutation",
4533
+ confidence: "deterministic",
4534
+ reason: "high_stakes_path",
4535
+ signals: ["high_stakes_path", "control_plane_path"]
4536
+ });
4537
+ }
4538
+ }
4539
+ }
4540
+ if (effect === "local_mutation" || effect === "unknown") {
4541
+ let destructiveHighStakes = false;
4542
+ for (const target of pathArgs) {
4543
+ const resolved = resolveTrustedPath(target, context.cwd, context.trustedCwd) ?? resolveMutationTarget(target, context.cwd);
4544
+ if (resolved && isDestructiveHighStakesMutation(
4545
+ segment.head,
4546
+ resolved,
4547
+ context.repoRoot,
4548
+ context.sensitivePaths,
4549
+ context.protectedArtifactRoots
4550
+ )) {
4551
+ destructiveHighStakes = true;
4552
+ break;
4553
+ }
4554
+ }
4555
+ if (destructiveHighStakes) {
4556
+ return askVerdict({
4557
+ location: pathAnalysis.location,
4558
+ opacity: "transparent",
4559
+ effect: "local_mutation",
4560
+ confidence: "deterministic",
4561
+ reason: "high_stakes_path",
4562
+ signals: pathAnalysis.signals
4563
+ });
4564
+ }
3372
4565
  }
3373
4566
  if (segment.head === "find" && isFindDangerous(peeled)) {
3374
4567
  return askVerdict({
@@ -3380,15 +4573,14 @@ async function evaluateSegment(command, context, depth) {
3380
4573
  signals: ["find_dangerous_action"]
3381
4574
  });
3382
4575
  }
3383
- if (pathAnalysis.location === "repo_outside" || pathAnalysis.location === "mixed") {
3384
- const outsideEffect = effect === "read_only" ? "read_only" : effect === "unknown" ? "local_mutation" : effect;
3385
- return askVerdict({
4576
+ if (effect === "read_only" && (pathAnalysis.location === "repo_outside" || pathAnalysis.location === "mixed")) {
4577
+ return allowVerdict({
3386
4578
  location: pathAnalysis.location,
3387
- opacity: "transparent",
3388
- effect: outsideEffect,
4579
+ opacity,
4580
+ effect: "read_only",
3389
4581
  confidence: "deterministic",
3390
- reason: "repo_outside_mutation",
3391
- signals: ["repo_outside_mutation", ...pathAnalysis.signals]
4582
+ reason: "read_only",
4583
+ signals: ["read_only", ...pathAnalysis.signals]
3392
4584
  });
3393
4585
  }
3394
4586
  if (pathAnalysis.location === "unknown" && pathArgs.length > 0 && LOCAL_MUTATION_KEYS.has(segment.head)) {
@@ -3401,7 +4593,25 @@ async function evaluateSegment(command, context, depth) {
3401
4593
  signals: ["unknown_location_mutation"]
3402
4594
  });
3403
4595
  }
3404
- const needsTier1 = effect === "unknown" || TIER0_EXTERNAL_HEADS.has(segment.head) || egressClass === "ambiguous";
4596
+ const outsideMutation = pathAnalysis.location === "repo_outside" || pathAnalysis.location === "mixed";
4597
+ const mutationPrescan = (effect === "local_mutation" || effect === "unknown") && pathArgs.length > 0 ? mutationPrescanRequiresAsk({
4598
+ targets: pathArgs,
4599
+ cwd: context.cwd,
4600
+ repoRoot: context.repoRoot,
4601
+ trustedCwd: context.trustedCwd,
4602
+ sensitivePaths: context.sensitivePaths
4603
+ }) : null;
4604
+ if (mutationPrescan) {
4605
+ return askVerdict({
4606
+ location: pathAnalysis.location === "unknown" ? "unknown" : pathAnalysis.location === "repo_outside" || pathAnalysis.location === "mixed" ? pathAnalysis.location : "repo_local",
4607
+ opacity,
4608
+ effect: "local_mutation",
4609
+ confidence: "deterministic",
4610
+ reason: "tier1_catastrophic",
4611
+ signals: ["tier1_catastrophic", mutationPrescan.reason]
4612
+ });
4613
+ }
4614
+ const needsTier1 = effect === "unknown" || TIER0_EXTERNAL_HEADS.has(segment.head) || egressClass === "ambiguous" || outsideMutation && effect !== "read_only";
3405
4615
  let tier1Trace;
3406
4616
  if (needsTier1) {
3407
4617
  const tier1Text = recursiveScript ?? command;
@@ -3413,9 +4623,9 @@ async function evaluateSegment(command, context, depth) {
3413
4623
  tier1Trace = context.judge.lastTrace;
3414
4624
  if (tier1RequiresAsk(tier1)) {
3415
4625
  return askVerdict({
3416
- location: pathAnalysis.location === "unknown" ? "unknown" : "repo_local",
4626
+ location: pathAnalysis.location === "unknown" ? "unknown" : pathAnalysis.location === "repo_outside" || pathAnalysis.location === "mixed" ? pathAnalysis.location : "repo_local",
3417
4627
  opacity,
3418
- effect: tier1.external_change ? "remote_mutation" : effect,
4628
+ effect: !tier1.local_recoverable ? "remote_mutation" : effect,
3419
4629
  confidence: "llm",
3420
4630
  reason: "tier1_catastrophic",
3421
4631
  signals: ["tier1_catastrophic", tier1.reason],
@@ -3423,6 +4633,19 @@ async function evaluateSegment(command, context, depth) {
3423
4633
  });
3424
4634
  }
3425
4635
  }
4636
+ if (outsideMutation && effect !== "read_only") {
4637
+ return withJudgeTrace(
4638
+ allowVerdict({
4639
+ location: pathAnalysis.location,
4640
+ opacity,
4641
+ effect: "local_mutation",
4642
+ confidence: tier1Trace ? "llm" : "deterministic",
4643
+ reason: "repo_outside_local_mutation",
4644
+ signals: ["repo_outside_local_mutation", ...pathAnalysis.signals]
4645
+ }),
4646
+ tier1Trace
4647
+ );
4648
+ }
3426
4649
  if (pathAnalysis.location === "repo_local" && (effect === "read_only" || effect === "local_mutation") && opacity !== "opaque") {
3427
4650
  return withJudgeTrace(
3428
4651
  allowVerdict({
@@ -3541,7 +4764,13 @@ async function verdict(command, context) {
3541
4764
  );
3542
4765
  }
3543
4766
 
3544
- // src/core/v2/adapter.ts
4767
+ // src/core/verdict/adapter.ts
4768
+ function resolveClassifierTrustedCwd(cwd, options, explicit) {
4769
+ if (explicit !== void 0) {
4770
+ return explicit;
4771
+ }
4772
+ return options?.trustedCwd ?? Boolean(cwd);
4773
+ }
3545
4774
  function buildVerdictContext(params) {
3546
4775
  const protectedArtifactRoots2 = [
3547
4776
  ...params.options?.protectedArtifactRoots ?? [],
@@ -3550,7 +4779,7 @@ function buildVerdictContext(params) {
3550
4779
  return {
3551
4780
  cwd: params.cwd,
3552
4781
  repoRoot: params.repoRoot,
3553
- trustedCwd: params.trustedCwd ?? Boolean(params.cwd),
4782
+ trustedCwd: resolveClassifierTrustedCwd(params.cwd, params.options, params.trustedCwd),
3554
4783
  sensitivePaths: params.options?.sensitivePaths ?? params.config.classifier.sensitivePaths,
3555
4784
  protectedArtifactRoots: protectedArtifactRoots2.length > 0 ? [...new Set(protectedArtifactRoots2)] : void 0,
3556
4785
  customAllowCommands: params.options?.customAllowCommands ?? params.config.overrides.allow,
@@ -3588,6 +4817,12 @@ function mapLegacyReason(result) {
3588
4817
  if (result.reason === "repo_local_mutation") {
3589
4818
  return "local_mutation";
3590
4819
  }
4820
+ if (result.reason === "tier1_not_restorable") {
4821
+ return "tier1_catastrophic";
4822
+ }
4823
+ if (result.reason === "tier0_restorable" || result.reason === "tier1_restorable") {
4824
+ return result.effect === "local_mutation" ? "local_mutation" : result.reason;
4825
+ }
3591
4826
  return result.reason;
3592
4827
  }
3593
4828
  function verdictToClassifyResult(result) {
@@ -3608,13 +4843,13 @@ function verdictToClassifyResult(result) {
3608
4843
  assessment,
3609
4844
  normalizedCommand: result.commandRedacted,
3610
4845
  summary: result.commandRedacted,
3611
- v2: {
4846
+ axes: {
3612
4847
  location: result.location,
3613
4848
  opacity: result.opacity,
3614
4849
  effect: result.effect,
3615
4850
  confidence: result.confidence,
3616
4851
  would: result.permission,
3617
- by: "v2",
4852
+ by: "verdict",
3618
4853
  commandRedacted: result.commandRedacted,
3619
4854
  commandFingerprint: result.fingerprint,
3620
4855
  signals: result.signals,
@@ -3676,7 +4911,7 @@ function extractPatch(payload) {
3676
4911
  }
3677
4912
  return null;
3678
4913
  }
3679
- function applyPatchTargets(patch) {
4914
+ function applyPatchTargets2(patch) {
3680
4915
  const targets = [];
3681
4916
  for (const line of patch.split("\n")) {
3682
4917
  const match = line.match(/^\*\*\* (Add|Delete|Update) File: (.+)$/);
@@ -3694,6 +4929,70 @@ function applyPatchTargets(patch) {
3694
4929
  function normalizedToolName(toolName) {
3695
4930
  return toolName.trim().toLowerCase();
3696
4931
  }
4932
+ function resolveFileTier1Judge(config, options, repoRoot) {
4933
+ return options.tier1Judge ?? createJudgeFromConfig(config, { repoRoot });
4934
+ }
4935
+ async function classifyFileMutationWithTier1(params) {
4936
+ const judge = resolveFileTier1Judge(params.config, params.options, params.repoRoot);
4937
+ const trustedCwd = resolveClassifierTrustedCwd(params.cwd, params.options);
4938
+ const prescan = mutationPrescanRequiresAsk({
4939
+ targets: [params.filePath],
4940
+ cwd: params.cwd,
4941
+ repoRoot: params.repoRoot,
4942
+ trustedCwd,
4943
+ sensitivePaths: params.options.sensitivePaths ?? params.config.classifier.sensitivePaths
4944
+ });
4945
+ if (prescan) {
4946
+ return {
4947
+ verdict: "deny_pending_approval",
4948
+ reason: "tier1_catastrophic",
4949
+ summary: params.filePath,
4950
+ fingerprint: toolFingerprint(params.toolName, { path: params.filePath }, params.repoRoot),
4951
+ assessment: {
4952
+ reversibility: "irreversible",
4953
+ external: params.locationLabel === "outside_repo",
4954
+ blastRadius: params.locationLabel === "outside_repo" ? "outside the repository" : "sensitive repository file",
4955
+ confidence: 0.95,
4956
+ signals: [...params.signals, "tier1_catastrophic", prescan.reason]
4957
+ }
4958
+ };
4959
+ }
4960
+ const tier1Text = `file_mutation: ${params.toolKind} path=${params.resolvedPath} location=${params.locationLabel}`;
4961
+ const tier1 = await judge.evaluate({
4962
+ text: tier1Text,
4963
+ context: { cwd: params.cwd, repoRoot: params.repoRoot }
4964
+ });
4965
+ const fingerprint = toolFingerprint(params.toolName, { path: params.filePath }, params.repoRoot);
4966
+ if (tier1RequiresAsk(tier1)) {
4967
+ return {
4968
+ verdict: "deny_pending_approval",
4969
+ reason: "tier1_catastrophic",
4970
+ summary: params.filePath,
4971
+ fingerprint,
4972
+ assessment: {
4973
+ reversibility: "irreversible",
4974
+ external: params.locationLabel === "outside_repo",
4975
+ blastRadius: params.locationLabel === "outside_repo" ? "outside the repository" : "sensitive repository file",
4976
+ confidence: 0.82,
4977
+ signals: [...params.signals, "tier1_catastrophic", tier1.reason]
4978
+ }
4979
+ };
4980
+ }
4981
+ const reason = params.isDelete ? "file_delete" : "file_mutation";
4982
+ return {
4983
+ verdict: "allow_flagged",
4984
+ reason,
4985
+ summary: params.filePath,
4986
+ fingerprint,
4987
+ assessment: {
4988
+ reversibility: "recoverable_with_cost",
4989
+ external: params.locationLabel === "outside_repo",
4990
+ blastRadius: params.locationLabel === "outside_repo" ? "outside the repository" : "this repository",
4991
+ confidence: 0.72,
4992
+ signals: [...params.signals, "tier1_restorable", tier1.reason]
4993
+ }
4994
+ };
4995
+ }
3697
4996
  async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
3698
4997
  const toolName = String(payload.tool_name ?? "");
3699
4998
  const toolKind = normalizedToolName(toolName);
@@ -3789,7 +5088,7 @@ async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
3789
5088
  };
3790
5089
  }
3791
5090
  const signals = [];
3792
- const resolvedPath = path12.isAbsolute(filePath) ? filePath : path12.resolve(cwd, filePath);
5091
+ const resolvedPath = path16.isAbsolute(filePath) ? filePath : path16.resolve(cwd, filePath);
3793
5092
  const hitsProtectedRoot = protectedRoots.some((root) => pathWithinRoot(root, resolvedPath));
3794
5093
  if (hitsProtectedRoot) {
3795
5094
  signals.push("control_plane_path");
@@ -3810,35 +5109,35 @@ async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
3810
5109
  const relativePath = relativeWithinRepo(repoRoot, resolvedPath);
3811
5110
  if (relativePath === null) {
3812
5111
  signals.push("outside_repo_path");
3813
- return {
3814
- verdict: "deny_pending_approval",
3815
- reason: "outside_repo_file_mutation",
3816
- summary: filePath,
3817
- fingerprint: toolFingerprint(toolName, { path: filePath }, repoRoot),
3818
- assessment: {
3819
- reversibility: "irreversible",
3820
- external: true,
3821
- blastRadius: "outside the repository",
3822
- confidence: 0.9,
3823
- signals
3824
- }
3825
- };
5112
+ return classifyFileMutationWithTier1({
5113
+ toolName,
5114
+ toolKind,
5115
+ filePath,
5116
+ resolvedPath,
5117
+ repoRoot,
5118
+ cwd,
5119
+ config,
5120
+ options,
5121
+ signals,
5122
+ isDelete: FILE_DELETE_TOOL_NAMES.has(toolKind),
5123
+ locationLabel: "outside_repo"
5124
+ });
3826
5125
  }
3827
5126
  if (matchesSensitivePath(relativePath, sensitivePaths)) {
3828
5127
  signals.push("sensitive_path");
3829
- return {
3830
- verdict: "deny_pending_approval",
3831
- reason: "sensitive_file_mutation",
3832
- summary: filePath,
3833
- fingerprint: toolFingerprint(toolName, { path: filePath }, repoRoot),
3834
- assessment: {
3835
- reversibility: "irreversible",
3836
- external: false,
3837
- blastRadius: "sensitive repository file",
3838
- confidence: 0.88,
3839
- signals
3840
- }
3841
- };
5128
+ return classifyFileMutationWithTier1({
5129
+ toolName,
5130
+ toolKind,
5131
+ filePath,
5132
+ resolvedPath,
5133
+ repoRoot,
5134
+ cwd,
5135
+ config,
5136
+ options,
5137
+ signals,
5138
+ isDelete: FILE_DELETE_TOOL_NAMES.has(toolKind),
5139
+ locationLabel: "sensitive_path"
5140
+ });
3842
5141
  }
3843
5142
  if (FILE_DELETE_TOOL_NAMES.has(toolKind)) {
3844
5143
  signals.push("file_delete");
@@ -3873,7 +5172,7 @@ async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
3873
5172
  }
3874
5173
  if (APPLY_PATCH_TOOL_NAMES.has(toolKind)) {
3875
5174
  const patch = extractPatch(payload);
3876
- const targets = patch ? applyPatchTargets(patch) : [];
5175
+ const targets = patch ? applyPatchTargets2(patch) : [];
3877
5176
  if (targets.length === 0) {
3878
5177
  if (options.unknownLocalEffect === "deny") {
3879
5178
  return {
@@ -4063,10 +5362,56 @@ function normalizeGatedAction(params) {
4063
5362
  agentAssessment
4064
5363
  };
4065
5364
  }
4066
- function applyShellPeripheralPolicy(command, action, result, options) {
4067
- if (options.brokerFsScope && result.verdict === "deny_pending_approval" && (result.reason === "outside_repo_mutation" || result.reason === "outside_repo_redirect" || result.reason === "repo_outside_mutation" || result.v2?.location === "repo_outside")) {
4068
- const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot);
4069
- if (outsideRepoPaths.length > 0 && options.fsScopeAllowlist && allPathsAllowlisted(outsideRepoPaths, options.fsScopeAllowlist)) {
5365
+ function applySandboxFsScopeBoundary(outsideRepoPaths, result, options, hints = {}) {
5366
+ if (!options.brokerFsScope) {
5367
+ return result;
5368
+ }
5369
+ if (outsideRepoPaths.length === 0) {
5370
+ return result;
5371
+ }
5372
+ if (options.fsScopeAllowlist && allPathsAllowlisted(outsideRepoPaths, options.fsScopeAllowlist)) {
5373
+ return result;
5374
+ }
5375
+ if (result.verdict === "deny_pending_approval") {
5376
+ return result;
5377
+ }
5378
+ const redirect = hints.redirect === true || result.reason === "read_only" || result.assessment.signals.includes("read_only") || result.reason === "outside_repo_redirect";
5379
+ return {
5380
+ ...result,
5381
+ verdict: "deny_pending_approval",
5382
+ reason: redirect ? "outside_repo_redirect" : "outside_repo_mutation",
5383
+ assessment: {
5384
+ ...result.assessment,
5385
+ external: true,
5386
+ reversibility: "irreversible",
5387
+ signals: [...result.assessment.signals, "sandbox_boundary_expected"]
5388
+ }
5389
+ };
5390
+ }
5391
+ function applyFsScopePeripheralPolicy(outsideRepoPaths, result, options) {
5392
+ if (!options.brokerFsScope) {
5393
+ return result;
5394
+ }
5395
+ if (outsideRepoPaths.length === 0) {
5396
+ return result;
5397
+ }
5398
+ if (options.fsScopeAllowlist && allPathsAllowlisted(outsideRepoPaths, options.fsScopeAllowlist)) {
5399
+ if (result.verdict === "deny_pending_approval" && (result.reason === "outside_repo_mutation" || result.reason === "outside_repo_redirect" || result.reason === "repo_outside_mutation" || result.axes?.location === "repo_outside")) {
5400
+ return {
5401
+ ...result,
5402
+ verdict: "allow_flagged",
5403
+ reason: "capability_fs_hint",
5404
+ assessment: {
5405
+ ...result.assessment,
5406
+ signals: [
5407
+ ...result.assessment.signals,
5408
+ "capability_fs_hint",
5409
+ "sandbox_boundary_expected"
5410
+ ]
5411
+ }
5412
+ };
5413
+ }
5414
+ if (result.verdict === "allow" || result.verdict === "allow_flagged") {
4070
5415
  return {
4071
5416
  ...result,
4072
5417
  verdict: "allow_flagged",
@@ -4084,6 +5429,38 @@ function applyShellPeripheralPolicy(command, action, result, options) {
4084
5429
  }
4085
5430
  return result;
4086
5431
  }
5432
+ function applySandboxOutsideBoundary(command, action, result, options) {
5433
+ const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot);
5434
+ return applySandboxFsScopeBoundary(outsideRepoPaths, result, options, {
5435
+ redirect: command.includes(">")
5436
+ });
5437
+ }
5438
+ function applyShellPeripheralPolicy(command, action, result, options) {
5439
+ const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot);
5440
+ return applyFsScopePeripheralPolicy(outsideRepoPaths, result, options);
5441
+ }
5442
+ function outsideRepoPathsForToolAction(action) {
5443
+ const payload = action.payload ?? {};
5444
+ const paths = new Set(
5445
+ collectOutsideRepoPathsFromToolPayload(payload, action.cwd, action.repoRoot)
5446
+ );
5447
+ const command = shellCommandFromPayload(payload);
5448
+ if (command) {
5449
+ for (const resolved of collectOutsideRepoPaths(command, action.cwd, action.repoRoot)) {
5450
+ paths.add(resolved);
5451
+ }
5452
+ }
5453
+ return [...paths];
5454
+ }
5455
+ function applyToolSandboxPolicies(action, result, options) {
5456
+ const outsideRepoPaths = outsideRepoPathsForToolAction(action);
5457
+ const command = shellCommandFromPayload(action.payload ?? {});
5458
+ let next = applySandboxFsScopeBoundary(outsideRepoPaths, result, options, {
5459
+ redirect: command.includes(">")
5460
+ });
5461
+ next = applyFsScopePeripheralPolicy(outsideRepoPaths, next, options);
5462
+ return next;
5463
+ }
4087
5464
  async function classifyGatedAction(action, config, extraOptions = {}) {
4088
5465
  const options = { ...classifierOptionsFromConfig(config), ...extraOptions };
4089
5466
  if (action.kind === "shell") {
@@ -4091,26 +5468,47 @@ async function classifyGatedAction(action, config, extraOptions = {}) {
4091
5468
  if (!command) {
4092
5469
  throw new GateNormalizationError("Shell gated action requires a command.");
4093
5470
  }
4094
- let result = await classifyShell(command, action.cwd, action.repoRoot, config, options);
4095
- result = applyShellPeripheralPolicy(command, action, result, options);
5471
+ let result2 = await classifyShell(command, action.cwd, action.repoRoot, config, options);
5472
+ result2 = applySandboxOutsideBoundary(command, action, result2, options);
5473
+ result2 = applyShellPeripheralPolicy(command, action, result2, options);
4096
5474
  if (!action.agentAssessment) {
4097
- return result;
5475
+ return result2;
4098
5476
  }
4099
- const merged = mergeAgentAssessment(result.assessment, action.agentAssessment);
4100
- if (!merged.mismatch) {
4101
- return { ...result, assessment: merged.assessment };
5477
+ const merged2 = mergeAgentAssessment(result2.assessment, action.agentAssessment);
5478
+ if (!merged2.mismatch) {
5479
+ return { ...result2, assessment: merged2.assessment };
4102
5480
  }
4103
5481
  return {
4104
- ...result,
5482
+ ...result2,
4105
5483
  verdict: "deny_pending_approval",
4106
5484
  reason: "agent_assessment_mismatch",
4107
- assessment: merged.assessment
5485
+ assessment: merged2.assessment
4108
5486
  };
4109
5487
  }
4110
5488
  if (action.kind === "subagent") {
4111
5489
  return classifySubagent(action.payload ?? {}, action.repoRoot, options);
4112
5490
  }
4113
- return classifyToolUse(action.payload ?? {}, action.repoRoot, action.cwd, config, options);
5491
+ let result = await classifyToolUse(
5492
+ action.payload ?? {},
5493
+ action.repoRoot,
5494
+ action.cwd,
5495
+ config,
5496
+ options
5497
+ );
5498
+ result = applyToolSandboxPolicies(action, result, options);
5499
+ if (!action.agentAssessment) {
5500
+ return result;
5501
+ }
5502
+ const merged = mergeAgentAssessment(result.assessment, action.agentAssessment);
5503
+ if (!merged.mismatch) {
5504
+ return { ...result, assessment: merged.assessment };
5505
+ }
5506
+ return {
5507
+ ...result,
5508
+ verdict: "deny_pending_approval",
5509
+ reason: "agent_assessment_mismatch",
5510
+ assessment: merged.assessment
5511
+ };
4114
5512
  }
4115
5513
  async function classifyGatedActionAsync(action, config, extraOptions = {}) {
4116
5514
  return classifyGatedAction(action, config, extraOptions);
@@ -4136,9 +5534,9 @@ function gateEnabledForAction(config, action) {
4136
5534
  }
4137
5535
 
4138
5536
  // src/core/transactional/diff-evaluator.ts
4139
- import path13 from "node:path";
5537
+ import path17 from "node:path";
4140
5538
  function categorizeChange(change, ctx) {
4141
- const absolutePath = canonicalPath(path13.join(ctx.repoRoot, change.relativePath));
5539
+ const absolutePath = canonicalPath(path17.join(ctx.repoRoot, change.relativePath));
4142
5540
  if (!pathWithinRoot(ctx.repoRoot, absolutePath)) {
4143
5541
  return "repo_outside";
4144
5542
  }
@@ -4160,15 +5558,6 @@ function observedAssessment(evaluation) {
4160
5558
  if (evaluation.deletedCount > 0) {
4161
5559
  signals.push("observed_deletions");
4162
5560
  }
4163
- if (evaluation.categories.includes("repo_outside") || evaluation.categories.includes("control_plane") || evaluation.categories.includes("sensitive_path")) {
4164
- return {
4165
- reversibility: "irreversible",
4166
- external: evaluation.categories.includes("repo_outside"),
4167
- blastRadius: evaluation.categories.includes("control_plane") ? "agent-belay control plane" : evaluation.categories.includes("repo_outside") ? "outside the repository" : "sensitive path",
4168
- confidence: 1,
4169
- signals
4170
- };
4171
- }
4172
5561
  if (evaluation.categories.includes("large_deletion")) {
4173
5562
  return {
4174
5563
  reversibility: "irreversible",
@@ -4196,7 +5585,7 @@ function evaluateTransactionalDiff(changes, ctx) {
4196
5585
  categories.add("large_deletion");
4197
5586
  }
4198
5587
  const categoryList = [...categories];
4199
- const dangerous = categoryList.includes("repo_outside") || categoryList.includes("control_plane") || categoryList.includes("sensitive_path") || categoryList.includes("large_deletion");
5588
+ const dangerous = categoryList.includes("large_deletion");
4200
5589
  const base = {
4201
5590
  categories: categoryList,
4202
5591
  changes,
@@ -4267,15 +5656,15 @@ var TRANSACTIONAL_APPROVAL_BYPASS_REASONS = /* @__PURE__ */ new Set([
4267
5656
  ]);
4268
5657
 
4269
5658
  // src/core/transactional/git-worktree.ts
4270
- import { spawn } from "node:child_process";
5659
+ import { spawn as spawn2 } from "node:child_process";
4271
5660
  import { randomUUID } from "node:crypto";
4272
5661
  import { existsSync as existsSync5 } from "node:fs";
4273
- import { copyFile, mkdir as mkdir2, mkdtemp, rm } from "node:fs/promises";
5662
+ import { copyFile, mkdir as mkdir3, mkdtemp, rm } from "node:fs/promises";
4274
5663
  import os from "node:os";
4275
- import path14 from "node:path";
5664
+ import path18 from "node:path";
4276
5665
  function execGit(repoRoot, args) {
4277
5666
  return new Promise((resolve, reject) => {
4278
- const child = spawn("git", ["-C", repoRoot, ...args], {
5667
+ const child = spawn2("git", ["-C", repoRoot, ...args], {
4279
5668
  stdio: ["ignore", "pipe", "pipe"]
4280
5669
  });
4281
5670
  const stdout = [];
@@ -4313,8 +5702,8 @@ async function isDirtyWorktree(repoRoot) {
4313
5702
  }
4314
5703
  }
4315
5704
  async function createGitWorktreeSnapshot(repoRoot, stateDir) {
4316
- const worktreePath = path14.join(stateDir, `tx-${randomUUID().replaceAll("-", "")}`);
4317
- await mkdir2(stateDir, { recursive: true });
5705
+ const worktreePath = path18.join(stateDir, `tx-${randomUUID().replaceAll("-", "")}`);
5706
+ await mkdir3(stateDir, { recursive: true });
4318
5707
  await execGit(repoRoot, ["worktree", "add", "--detach", worktreePath, "HEAD"]);
4319
5708
  return {
4320
5709
  worktreePath,
@@ -4333,18 +5722,18 @@ async function createGitWorktreeSnapshot(repoRoot, stateDir) {
4333
5722
  }
4334
5723
  function resolveWorktreeCwd(repoRoot, worktreePath, cwd) {
4335
5724
  const resolvedCwd = canonicalPath(cwd);
4336
- const relative = path14.relative(canonicalPath(repoRoot), resolvedCwd);
4337
- if (relative.startsWith("..") || path14.isAbsolute(relative)) {
5725
+ const relative = path18.relative(canonicalPath(repoRoot), resolvedCwd);
5726
+ if (relative.startsWith("..") || path18.isAbsolute(relative)) {
4338
5727
  return worktreePath;
4339
5728
  }
4340
5729
  if (relative === "") {
4341
5730
  return worktreePath;
4342
5731
  }
4343
- return path14.join(worktreePath, relative);
5732
+ return path18.join(worktreePath, relative);
4344
5733
  }
4345
5734
  function runShellCommand(command, cwd, timeoutMs) {
4346
5735
  return new Promise((resolve) => {
4347
- const child = spawn(command, {
5736
+ const child = spawn2(command, {
4348
5737
  cwd,
4349
5738
  shell: true,
4350
5739
  stdio: "ignore",
@@ -4410,7 +5799,7 @@ async function rollbackAppliedChanges(actions) {
4410
5799
  for (const action of [...actions].reverse()) {
4411
5800
  try {
4412
5801
  if (action.type === "restore") {
4413
- await mkdir2(path14.dirname(action.target), { recursive: true });
5802
+ await mkdir3(path18.dirname(action.target), { recursive: true });
4414
5803
  await copyFile(action.backupPath, action.target);
4415
5804
  } else {
4416
5805
  await rm(action.target, { force: true });
@@ -4420,14 +5809,14 @@ async function rollbackAppliedChanges(actions) {
4420
5809
  }
4421
5810
  }
4422
5811
  async function applyWorktreeChanges(worktreePath, repoRoot, changes) {
4423
- const backupRoot = await mkdtemp(path14.join(os.tmpdir(), "belay-tx-rollback-"));
5812
+ const backupRoot = await mkdtemp(path18.join(os.tmpdir(), "belay-tx-rollback-"));
4424
5813
  const rollbackActions = [];
4425
5814
  try {
4426
5815
  for (const change of changes) {
4427
- const target = path14.join(repoRoot, change.relativePath);
5816
+ const target = path18.join(repoRoot, change.relativePath);
4428
5817
  if (existsSync5(target)) {
4429
- const backupPath = path14.join(backupRoot, change.relativePath);
4430
- await mkdir2(path14.dirname(backupPath), { recursive: true });
5818
+ const backupPath = path18.join(backupRoot, change.relativePath);
5819
+ await mkdir3(path18.dirname(backupPath), { recursive: true });
4431
5820
  await copyFile(target, backupPath);
4432
5821
  rollbackActions.push({ type: "restore", target, backupPath });
4433
5822
  } else if (change.kind !== "deleted") {
@@ -4437,8 +5826,8 @@ async function applyWorktreeChanges(worktreePath, repoRoot, changes) {
4437
5826
  await rm(target, { force: true });
4438
5827
  continue;
4439
5828
  }
4440
- const source = path14.join(worktreePath, change.relativePath);
4441
- await mkdir2(path14.dirname(target), { recursive: true });
5829
+ const source = path18.join(worktreePath, change.relativePath);
5830
+ await mkdir3(path18.dirname(target), { recursive: true });
4442
5831
  await copyFile(source, target);
4443
5832
  }
4444
5833
  } catch (error) {
@@ -4600,7 +5989,7 @@ async function notifyDeny(config, event) {
4600
5989
  }
4601
5990
 
4602
5991
  // src/adapters/layouts/protected-paths.ts
4603
- import path15 from "node:path";
5992
+ import path19 from "node:path";
4604
5993
  function protectedArtifactRoots(layout, repoRoot, controlPlaneDir) {
4605
5994
  const roots = [
4606
5995
  layout.configPath(repoRoot),
@@ -4612,7 +6001,7 @@ function protectedArtifactRoots(layout, repoRoot, controlPlaneDir) {
4612
6001
  if (controlPlaneDir) {
4613
6002
  roots.push(controlPlaneDir);
4614
6003
  }
4615
- return roots.map((entry) => path15.resolve(entry));
6004
+ return roots.map((entry) => path19.resolve(entry));
4616
6005
  }
4617
6006
 
4618
6007
  // src/adapters/shared/gate-runtime.ts
@@ -4622,7 +6011,7 @@ var EMPTY_APPROVALS = {
4622
6011
  };
4623
6012
  async function loadJsonFile(filePath, fallback) {
4624
6013
  try {
4625
- const raw = await readFile2(filePath, "utf8");
6014
+ const raw = await readFile3(filePath, "utf8");
4626
6015
  return JSON.parse(raw);
4627
6016
  } catch {
4628
6017
  return fallback;
@@ -4634,14 +6023,14 @@ function createDefaultGateRuntimeDeps() {
4634
6023
  return loadJsonFile(configPath, {});
4635
6024
  },
4636
6025
  async appendAudit(ctx, event) {
4637
- const auditPath = path16.join(ctx.repoRoot, ctx.config.audit.logPath);
4638
- await mkdir3(path16.dirname(auditPath), { recursive: true });
6026
+ const auditPath = path20.join(ctx.repoRoot, ctx.config.audit.logPath);
6027
+ await mkdir4(path20.dirname(auditPath), { recursive: true });
4639
6028
  const record = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), ...event };
4640
6029
  if (!ctx.config.audit.includeAssessment) {
4641
6030
  delete record.assessment;
4642
6031
  }
4643
6032
  const scrubbed = scrubValue(record, scrubOptionsFromConfig(ctx.config));
4644
- await writeFile2(auditPath, `${JSON.stringify(scrubbed)}
6033
+ await writeFile3(auditPath, `${JSON.stringify(scrubbed)}
4645
6034
  `, {
4646
6035
  encoding: "utf8",
4647
6036
  flag: "a"
@@ -4660,8 +6049,8 @@ function createDefaultGateRuntimeDeps() {
4660
6049
  };
4661
6050
  },
4662
6051
  async writeApprovals(filePath, state) {
4663
- await mkdir3(path16.dirname(filePath), { recursive: true });
4664
- await writeFile2(filePath, `${JSON.stringify(compactApprovals(state), null, 2)}
6052
+ await mkdir4(path20.dirname(filePath), { recursive: true });
6053
+ await writeFile3(filePath, `${JSON.stringify(compactApprovals(state), null, 2)}
4665
6054
  `, "utf8");
4666
6055
  }
4667
6056
  };
@@ -4671,7 +6060,7 @@ async function resolveGateConfig(ctx, deps) {
4671
6060
  let teamConfig = null;
4672
6061
  const teamPath = teamConfigPath();
4673
6062
  if (existsSync6(teamPath)) {
4674
- teamConfig = JSON.parse(await readFile2(teamPath, "utf8"));
6063
+ teamConfig = JSON.parse(await readFile3(teamPath, "utf8"));
4675
6064
  }
4676
6065
  return resolveLayeredConfig({
4677
6066
  repoConfig: loaded,
@@ -4743,7 +6132,15 @@ async function consumeApprovedApproval(ctx, deps, kind, fingerprint) {
4743
6132
  await deps.writeApprovals(approved.filePath, approved.state);
4744
6133
  return null;
4745
6134
  }
4746
- const [approval] = approved.state.approvals.splice(index, 1);
6135
+ const approval = approved.state.approvals[index];
6136
+ if (approval.executionLeaseExpiresAt) {
6137
+ await deps.writeApprovals(approved.filePath, approved.state);
6138
+ return approval;
6139
+ }
6140
+ approved.state.approvals[index] = {
6141
+ ...approval,
6142
+ executionLeaseExpiresAt: new Date(Date.now() + APPROVAL_EXECUTION_LEASE_MS).toISOString()
6143
+ };
4747
6144
  await deps.writeApprovals(approved.filePath, approved.state);
4748
6145
  return approval;
4749
6146
  }
@@ -4808,7 +6205,7 @@ async function evaluateGatedAction(ctx, deps, params) {
4808
6205
  command: params.command,
4809
6206
  cwd: params.cwd,
4810
6207
  repoRoot: ctx.repoRoot,
4811
- stateDir: path16.join(ctx.layout.repoLocalStateDir(ctx.repoRoot), "transactional"),
6208
+ stateDir: path20.join(ctx.layout.repoLocalStateDir(ctx.repoRoot), "transactional"),
4812
6209
  timeoutMs: transactional.timeoutMs,
4813
6210
  predicted,
4814
6211
  diffContext: {
@@ -4859,8 +6256,8 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
4859
6256
  predictedAssessment: auditExtras.predictedAssessment,
4860
6257
  observedAssessment: auditExtras.observedAssessment,
4861
6258
  mode: ctx.config.mode,
4862
- schemaVersion: result.v2 ? 2 : 1,
4863
- ...result.v2 ?? {},
6259
+ schemaVersion: result.axes ? 2 : 1,
6260
+ ...result.axes ?? {},
4864
6261
  ...auditExtras.transactionalLayer
4865
6262
  };
4866
6263
  if (result.reason === TRANSACTIONAL_ALREADY_APPLIED) {
@@ -4883,7 +6280,17 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
4883
6280
  });
4884
6281
  }
4885
6282
  const brokerActive = isCapabilityBrokerDemotionActive(ctx.config);
4886
- const approved = TRANSACTIONAL_APPROVAL_BYPASS_REASONS.has(result.reason) || shouldSkipBrokerApprovedOnce(brokerActive, result.reason) ? null : await consumeApprovedApproval(ctx, deps, kind, result.fingerprint);
6283
+ let approved = null;
6284
+ if (!TRANSACTIONAL_APPROVAL_BYPASS_REASONS.has(result.reason) && !shouldSkipBrokerApprovedOnce(brokerActive, result.reason)) {
6285
+ const approvedState = await deps.loadApprovals(ctx, "approved-approvals.json");
6286
+ approvedState.state = compactApprovals(approvedState.state);
6287
+ const matchedApproval = approvedState.state.approvals.find(
6288
+ (entry) => entry.kind === kind && entry.fingerprint === result.fingerprint && entry.repoRoot === ctx.repoRoot
6289
+ );
6290
+ if (!shouldSkipBrokerApprovedRecord(brokerActive, matchedApproval?.reason)) {
6291
+ approved = await consumeApprovedApproval(ctx, deps, kind, result.fingerprint);
6292
+ }
6293
+ }
4887
6294
  if (approved) {
4888
6295
  await deps.appendAudit(ctx, {
4889
6296
  ...gateBase,
@@ -5061,18 +6468,18 @@ async function appendObservedAudit(ctx, deps, eventName, payload) {
5061
6468
 
5062
6469
  // src/adapters/shared/repo-root.ts
5063
6470
  import { existsSync as existsSync7 } from "node:fs";
5064
- import path17 from "node:path";
6471
+ import path21 from "node:path";
5065
6472
  function findRepoRoot(startPath, layout) {
5066
- let current = path17.resolve(startPath);
6473
+ let current = path21.resolve(startPath);
5067
6474
  while (true) {
5068
6475
  for (const marker of layout.repoRootMarkers) {
5069
- if (existsSync7(path17.join(current, marker))) {
6476
+ if (existsSync7(path21.join(current, marker))) {
5070
6477
  return current;
5071
6478
  }
5072
6479
  }
5073
- const parent = path17.dirname(current);
6480
+ const parent = path21.dirname(current);
5074
6481
  if (parent === current) {
5075
- return path17.resolve(startPath);
6482
+ return path21.resolve(startPath);
5076
6483
  }
5077
6484
  current = parent;
5078
6485
  }