@guilz-dev/belay 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/dist/adapters/cursor/runtime-entry.js +1 -0
  2. package/dist/adapters/shared/gate-runtime.d.ts +11 -14
  3. package/dist/adapters/shared/gate-runtime.js +277 -19
  4. package/dist/bundle/claude-runtime.mjs +3138 -699
  5. package/dist/bundle/codex-runtime.mjs +3162 -711
  6. package/dist/bundle/cursor-runtime.mjs +3140 -701
  7. package/dist/cli.js +137 -5
  8. package/dist/commands/approve.d.ts +1 -0
  9. package/dist/commands/approve.js +45 -26
  10. package/dist/commands/config.js +135 -52
  11. package/dist/commands/doctor.js +11 -1
  12. package/dist/commands/explain.js +6 -1
  13. package/dist/commands/harvest.d.ts +29 -0
  14. package/dist/commands/harvest.js +72 -0
  15. package/dist/commands/judge.d.ts +11 -1
  16. package/dist/commands/judge.js +21 -2
  17. package/dist/commands/metrics.js +34 -2
  18. package/dist/commands/quality.d.ts +38 -0
  19. package/dist/commands/quality.js +105 -0
  20. package/dist/commands/simulate.d.ts +1 -0
  21. package/dist/commands/simulate.js +7 -2
  22. package/dist/commands/standing-allow.d.ts +10 -0
  23. package/dist/commands/standing-allow.js +26 -0
  24. package/dist/commands/tui.d.ts +48 -0
  25. package/dist/commands/tui.js +150 -0
  26. package/dist/config-io.js +10 -5
  27. package/dist/core/approval-replay-cli.d.ts +7 -0
  28. package/dist/core/approval-replay-cli.js +36 -0
  29. package/dist/core/approval-replay.d.ts +45 -0
  30. package/dist/core/approval-replay.js +141 -0
  31. package/dist/core/approval-service.d.ts +15 -0
  32. package/dist/core/approval-service.js +47 -2
  33. package/dist/core/approval.d.ts +24 -3
  34. package/dist/core/approval.js +47 -4
  35. package/dist/core/audit-analysis.d.ts +7 -1
  36. package/dist/core/audit-analysis.js +111 -0
  37. package/dist/core/audit-metrics.d.ts +7 -0
  38. package/dist/core/audit-metrics.js +24 -4
  39. package/dist/core/audit-query.d.ts +1 -0
  40. package/dist/core/audit-query.js +3 -0
  41. package/dist/core/audit-replay-context.d.ts +35 -0
  42. package/dist/core/audit-replay-context.js +88 -0
  43. package/dist/core/audit-types.d.ts +24 -1
  44. package/dist/core/audit-types.js +1 -1
  45. package/dist/core/capability/index.d.ts +2 -1
  46. package/dist/core/capability/index.js +1 -0
  47. package/dist/core/capability/paths.d.ts +2 -2
  48. package/dist/core/capability/paths.js +9 -9
  49. package/dist/core/capability/trusted-workspace-roots.d.ts +25 -0
  50. package/dist/core/capability/trusted-workspace-roots.js +149 -0
  51. package/dist/core/capability/types.d.ts +11 -1
  52. package/dist/core/capability-approval.d.ts +2 -1
  53. package/dist/core/capability-approval.js +100 -2
  54. package/dist/core/classify-subagent.js +2 -20
  55. package/dist/core/classify-tool.js +32 -3
  56. package/dist/core/config.d.ts +18 -0
  57. package/dist/core/config.js +49 -0
  58. package/dist/core/gate-engine.js +6 -6
  59. package/dist/core/harvest.d.ts +53 -0
  60. package/dist/core/harvest.js +276 -0
  61. package/dist/core/index.d.ts +9 -3
  62. package/dist/core/index.js +6 -2
  63. package/dist/core/judge-doctor.d.ts +3 -0
  64. package/dist/core/judge-doctor.js +54 -0
  65. package/dist/core/path-utils.d.ts +11 -0
  66. package/dist/core/path-utils.js +56 -6
  67. package/dist/core/reclassify.d.ts +3 -0
  68. package/dist/core/reclassify.js +44 -14
  69. package/dist/core/replay-scrub.d.ts +9 -0
  70. package/dist/core/replay-scrub.js +43 -0
  71. package/dist/core/shell-tokenizer.d.ts +2 -0
  72. package/dist/core/shell-tokenizer.js +82 -23
  73. package/dist/core/standing-allow.d.ts +50 -0
  74. package/dist/core/standing-allow.js +175 -0
  75. package/dist/core/types.d.ts +15 -0
  76. package/dist/core/verdict/adapter.js +10 -1
  77. package/dist/core/verdict/containment.d.ts +4 -3
  78. package/dist/core/verdict/containment.js +17 -12
  79. package/dist/core/verdict/judge-audit.d.ts +1 -0
  80. package/dist/core/verdict/judge-audit.js +32 -1
  81. package/dist/core/verdict/judge-baseline.d.ts +20 -0
  82. package/dist/core/verdict/judge-baseline.js +48 -0
  83. package/dist/core/verdict/judge-broker-service.d.ts +42 -0
  84. package/dist/core/verdict/judge-broker-service.js +279 -0
  85. package/dist/core/verdict/judge-cli-fingerprint.d.ts +5 -0
  86. package/dist/core/verdict/judge-cli-fingerprint.js +61 -0
  87. package/dist/core/verdict/judge-cli.d.ts +23 -0
  88. package/dist/core/verdict/judge-cli.js +137 -16
  89. package/dist/core/verdict/judge-factory.js +13 -4
  90. package/dist/core/verdict/judge-provider-matrix.d.ts +20 -0
  91. package/dist/core/verdict/judge-provider-matrix.js +66 -0
  92. package/dist/core/verdict/judge-runtime-config.d.ts +42 -0
  93. package/dist/core/verdict/judge-runtime-config.js +92 -0
  94. package/dist/core/verdict/judge-session-broker.d.ts +48 -0
  95. package/dist/core/verdict/judge-session-broker.js +195 -0
  96. package/dist/core/verdict/judge-session-guard.d.ts +27 -0
  97. package/dist/core/verdict/judge-session-guard.js +91 -0
  98. package/dist/core/verdict/judge-session-kill-switch.d.ts +11 -0
  99. package/dist/core/verdict/judge-session-kill-switch.js +43 -0
  100. package/dist/core/verdict/judge-session-mutex.d.ts +9 -0
  101. package/dist/core/verdict/judge-session-mutex.js +23 -0
  102. package/dist/core/verdict/judge-shadow.d.ts +24 -0
  103. package/dist/core/verdict/judge-shadow.js +116 -0
  104. package/dist/core/verdict/judge-transport.d.ts +40 -0
  105. package/dist/core/verdict/judge-transport.js +258 -0
  106. package/dist/core/verdict/judge.d.ts +14 -1
  107. package/dist/core/verdict/judge.js +6 -5
  108. package/dist/core/verdict/parser.js +6 -1
  109. package/dist/core/verdict/types.d.ts +13 -0
  110. package/dist/core/verdict/verdict.js +7 -4
  111. package/dist/corpus/adversarial-probe.d.ts +86 -0
  112. package/dist/corpus/adversarial-probe.js +220 -0
  113. package/dist/corpus/evaluate.d.ts +45 -12
  114. package/dist/corpus/evaluate.js +62 -5
  115. package/dist/corpus/gates.d.ts +35 -0
  116. package/dist/corpus/gates.js +81 -0
  117. package/dist/corpus/judge-accuracy.d.ts +8 -0
  118. package/dist/corpus/judge-accuracy.js +39 -0
  119. package/dist/corpus/must-allow-commands.d.ts +5 -0
  120. package/dist/corpus/must-allow-commands.js +13 -0
  121. package/dist/corpus/mutators.d.ts +28 -0
  122. package/dist/corpus/mutators.js +151 -0
  123. package/dist/corpus/ratchet.d.ts +42 -0
  124. package/dist/corpus/ratchet.js +116 -0
  125. package/dist/corpus/runtime-match.d.ts +19 -0
  126. package/dist/corpus/runtime-match.js +45 -0
  127. package/dist/corpus/standing-allow-catalog.generated.d.ts +13 -0
  128. package/dist/corpus/standing-allow-catalog.generated.js +79 -0
  129. package/dist/corpus/types.d.ts +47 -0
  130. package/dist/corpus/types.js +157 -0
  131. package/dist/installer.js +4 -1
  132. package/dist/judge-broker-daemon.d.ts +1 -0
  133. package/dist/judge-broker-daemon.js +123 -0
  134. package/dist/services/sandbox-service.d.ts +1 -0
  135. package/dist/services/sandbox-service.js +2 -0
  136. package/dist/version.d.ts +1 -1
  137. package/dist/version.js +1 -1
  138. package/package.json +7 -3
  139. package/skills/belay/SKILL.md +7 -1
  140. package/skills/belay/belay-approve.md +17 -0
@@ -196,6 +196,115 @@ function isCloudProviderId(providerId) {
196
196
  return JUDGE_CATALOG[normalized].isCloud;
197
197
  }
198
198
 
199
+ // src/core/verdict/judge-runtime-config.ts
200
+ var DEFAULT_JUDGE_SESSION_CONFIG = {
201
+ enabled: false,
202
+ maxTurns: 32,
203
+ maxAgeMs: 30 * 60 * 1e3,
204
+ maxIdleMs: 5 * 60 * 1e3,
205
+ maxPromptBytes: 64 * 1024,
206
+ providerAllowlist: ["cursor"],
207
+ connectTimeoutMs: 5e3,
208
+ evalTimeoutMs: null,
209
+ parseTimeoutMs: 2e3,
210
+ maxSessionsPerProvider: 1
211
+ };
212
+ var DEFAULT_JUDGE_SHADOW_CONFIG = {
213
+ enabled: false,
214
+ sampleRate: 0.01,
215
+ sampleRateMax: 0.05,
216
+ dailyRequestCap: 500,
217
+ mismatchRateThreshold: 0.02,
218
+ providerAllowlist: ["cursor"],
219
+ windowSize: 100
220
+ };
221
+ var DEFAULT_JUDGE_RUNTIME_CONFIG = {
222
+ session: { ...DEFAULT_JUDGE_SESSION_CONFIG },
223
+ shadow: { ...DEFAULT_JUDGE_SHADOW_CONFIG }
224
+ };
225
+ var CLI_PROVIDER_IDS = ["codex", "cursor", "claude"];
226
+ function normalizeProviderAllowlist(value, fallback) {
227
+ if (!Array.isArray(value)) {
228
+ return [...fallback];
229
+ }
230
+ const allowed = value.map((entry) => String(entry).trim()).filter(
231
+ (entry) => CLI_PROVIDER_IDS.includes(entry)
232
+ );
233
+ return allowed.length > 0 ? [...new Set(allowed)] : [...fallback];
234
+ }
235
+ function normalizePositiveInt(value, fallback) {
236
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
237
+ }
238
+ function normalizeRate(value, fallback, max) {
239
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
240
+ return fallback;
241
+ }
242
+ return Math.min(value, max);
243
+ }
244
+ function normalizeJudgeRuntimeConfig(runtime) {
245
+ const session = runtime?.session ?? {};
246
+ const shadow = runtime?.shadow ?? {};
247
+ return {
248
+ session: normalizeJudgeSessionConfig(session),
249
+ shadow: {
250
+ enabled: shadow.enabled === true,
251
+ sampleRate: normalizeRate(shadow.sampleRate, DEFAULT_JUDGE_SHADOW_CONFIG.sampleRate, 1),
252
+ sampleRateMax: normalizeRate(
253
+ shadow.sampleRateMax,
254
+ DEFAULT_JUDGE_SHADOW_CONFIG.sampleRateMax,
255
+ 1
256
+ ),
257
+ dailyRequestCap: normalizePositiveInt(
258
+ shadow.dailyRequestCap,
259
+ DEFAULT_JUDGE_SHADOW_CONFIG.dailyRequestCap
260
+ ),
261
+ mismatchRateThreshold: normalizeRate(
262
+ shadow.mismatchRateThreshold,
263
+ DEFAULT_JUDGE_SHADOW_CONFIG.mismatchRateThreshold,
264
+ 1
265
+ ),
266
+ providerAllowlist: normalizeProviderAllowlist(
267
+ shadow.providerAllowlist,
268
+ DEFAULT_JUDGE_SHADOW_CONFIG.providerAllowlist
269
+ ),
270
+ windowSize: normalizePositiveInt(shadow.windowSize, DEFAULT_JUDGE_SHADOW_CONFIG.windowSize)
271
+ }
272
+ };
273
+ }
274
+ function normalizeJudgeSessionConfig(session) {
275
+ const input = session ?? {};
276
+ return {
277
+ enabled: input.enabled === true,
278
+ maxTurns: normalizePositiveInt(input.maxTurns, DEFAULT_JUDGE_SESSION_CONFIG.maxTurns),
279
+ maxAgeMs: normalizePositiveInt(input.maxAgeMs, DEFAULT_JUDGE_SESSION_CONFIG.maxAgeMs),
280
+ maxIdleMs: normalizePositiveInt(input.maxIdleMs, DEFAULT_JUDGE_SESSION_CONFIG.maxIdleMs),
281
+ maxPromptBytes: normalizePositiveInt(
282
+ input.maxPromptBytes,
283
+ DEFAULT_JUDGE_SESSION_CONFIG.maxPromptBytes
284
+ ),
285
+ providerAllowlist: normalizeProviderAllowlist(
286
+ input.providerAllowlist,
287
+ DEFAULT_JUDGE_SESSION_CONFIG.providerAllowlist
288
+ ),
289
+ connectTimeoutMs: normalizePositiveInt(
290
+ input.connectTimeoutMs,
291
+ DEFAULT_JUDGE_SESSION_CONFIG.connectTimeoutMs
292
+ ),
293
+ evalTimeoutMs: typeof input.evalTimeoutMs === "number" && input.evalTimeoutMs > 0 ? Math.floor(input.evalTimeoutMs) : null,
294
+ parseTimeoutMs: normalizePositiveInt(
295
+ input.parseTimeoutMs,
296
+ DEFAULT_JUDGE_SESSION_CONFIG.parseTimeoutMs
297
+ ),
298
+ maxSessionsPerProvider: normalizePositiveInt(
299
+ input.maxSessionsPerProvider,
300
+ DEFAULT_JUDGE_SESSION_CONFIG.maxSessionsPerProvider
301
+ )
302
+ };
303
+ }
304
+ function resolveSessionEvalTimeoutMs(sessionConfig, judgeTimeoutMs) {
305
+ return sessionConfig.evalTimeoutMs ?? judgeTimeoutMs;
306
+ }
307
+
199
308
  // src/core/audit-summary.ts
200
309
  var DEFAULT_SILENT_PASS_THRESHOLD = 0.5;
201
310
 
@@ -288,6 +397,15 @@ var DEFAULT_NOTIFICATIONS_V3 = {};
288
397
  var DEFAULT_APPROVAL_SIGNING_V3 = {
289
398
  required: false
290
399
  };
400
+ var DEFAULT_APPROVAL_CONFIG = {
401
+ flow: "one_step",
402
+ autoReplayScopes: {
403
+ shell: true,
404
+ tool: false,
405
+ subagent: false
406
+ },
407
+ executionLeaseMs: 6e4
408
+ };
291
409
  var DEFAULT_EGRESS_V3 = {
292
410
  enabled: false,
293
411
  listenHost: "127.0.0.1",
@@ -341,6 +459,7 @@ var DEFAULT_CONFIG_V4 = {
341
459
  controlPlane: { ...DEFAULT_CONTROL_PLANE_V3 },
342
460
  notifications: { ...DEFAULT_NOTIFICATIONS_V3 },
343
461
  approvalSigning: { ...DEFAULT_APPROVAL_SIGNING_V3 },
462
+ approval: { ...DEFAULT_APPROVAL_CONFIG },
344
463
  egress: { ...DEFAULT_EGRESS_V3 },
345
464
  sandbox: { ...DEFAULT_SANDBOX_V3 },
346
465
  audit: { ...DEFAULT_CONFIG_V2.audit },
@@ -380,6 +499,7 @@ function migrateV2ToV3(v2, rawOverrides) {
380
499
  controlPlane: { ...LEGACY_CONTROL_PLANE_V3 },
381
500
  notifications: { ...DEFAULT_NOTIFICATIONS_V3 },
382
501
  approvalSigning: { ...DEFAULT_APPROVAL_SIGNING_V3 },
502
+ approval: { ...DEFAULT_APPROVAL_CONFIG },
383
503
  egress: { ...DEFAULT_EGRESS_V3 },
384
504
  sandbox: { ...DEFAULT_SANDBOX_V3 },
385
505
  audit: v2.audit,
@@ -448,7 +568,8 @@ function normalizeJudgeConfig(judge) {
448
568
  endpoint: endpoint2,
449
569
  keepAlive: provider === "ollama" && typeof judge.keepAlive === "string" && judge.keepAlive.trim() ? judge.keepAlive.trim() : provider === "ollama" ? DEFAULT_JUDGE_LOCAL_OLLAMA.keepAlive : null,
450
570
  ...judge.cloudConsent ? { cloudConsent: judge.cloudConsent } : {},
451
- ...judge.credential ? { credential: judge.credential } : {}
571
+ ...judge.credential ? { credential: judge.credential } : {},
572
+ runtime: normalizeJudgeRuntimeConfig(judge.runtime)
452
573
  };
453
574
  }
454
575
  let providerId = rawProviderId && normalizeLegacyProviderId(rawProviderId) ? normalizeLegacyProviderId(rawProviderId) : inferProviderIdFromConfig({ ...judge, provider });
@@ -493,6 +614,7 @@ function normalizeJudgeConfig(judge) {
493
614
  ...judge.credential.ref ? { ref: judge.credential.ref } : {}
494
615
  };
495
616
  }
617
+ normalized.runtime = normalizeJudgeRuntimeConfig(judge.runtime);
496
618
  return normalized;
497
619
  }
498
620
  function rejectTeamLayerJudgeSecrets(judge, source) {
@@ -541,6 +663,14 @@ function mergeV3FromRaw(base, raw) {
541
663
  ...base.approvalSigning,
542
664
  ...raw.approvalSigning ?? {}
543
665
  },
666
+ approval: {
667
+ ...base.approval,
668
+ ...raw.approval ?? {},
669
+ autoReplayScopes: {
670
+ ...base.approval.autoReplayScopes,
671
+ ...raw.approval?.autoReplayScopes ?? {}
672
+ }
673
+ },
544
674
  egress: {
545
675
  ...base.egress,
546
676
  ...raw.egress ?? {}
@@ -592,6 +722,14 @@ function normalizeV3Raw(raw) {
592
722
  approvalSigning: {
593
723
  required: raw.approvalSigning?.required === true
594
724
  },
725
+ approval: {
726
+ ...DEFAULT_APPROVAL_CONFIG,
727
+ ...raw.approval ?? {},
728
+ autoReplayScopes: {
729
+ ...DEFAULT_APPROVAL_CONFIG.autoReplayScopes,
730
+ ...raw.approval?.autoReplayScopes ?? {}
731
+ }
732
+ },
595
733
  egress: {
596
734
  ...DEFAULT_EGRESS_V3,
597
735
  ...raw.egress ?? {}
@@ -761,6 +899,15 @@ function normalizeConfig(config) {
761
899
  approvalSigning: {
762
900
  required: v4.approvalSigning?.required === true
763
901
  },
902
+ approval: {
903
+ flow: v4.approval?.flow === "two_step" ? "two_step" : DEFAULT_APPROVAL_CONFIG.flow,
904
+ autoReplayScopes: {
905
+ shell: v4.approval?.autoReplayScopes?.shell !== false,
906
+ tool: v4.approval?.autoReplayScopes?.tool === true,
907
+ subagent: v4.approval?.autoReplayScopes?.subagent === true
908
+ },
909
+ executionLeaseMs: typeof v4.approval?.executionLeaseMs === "number" && v4.approval.executionLeaseMs > 0 ? v4.approval.executionLeaseMs : DEFAULT_APPROVAL_CONFIG.executionLeaseMs
910
+ },
764
911
  egress: {
765
912
  enabled: v4.egress?.enabled === true,
766
913
  listenHost: normalizeEgressListenHost(
@@ -828,6 +975,14 @@ function mergeConfig(existing, defaults = DEFAULT_CONFIG_V4) {
828
975
  ...defaults.approvalSigning,
829
976
  ...migrated.approvalSigning
830
977
  },
978
+ approval: {
979
+ ...defaults.approval,
980
+ ...migrated.approval,
981
+ autoReplayScopes: {
982
+ ...defaults.approval.autoReplayScopes,
983
+ ...migrated.approval.autoReplayScopes
984
+ }
985
+ },
831
986
  egress: {
832
987
  ...defaults.egress,
833
988
  ...migrated.egress
@@ -945,12 +1100,216 @@ var claudeLayout = {
945
1100
 
946
1101
  // src/adapters/shared/gate-runtime.ts
947
1102
  import { randomUUID as randomUUID2 } from "node:crypto";
948
- import { existsSync as existsSync6 } from "node:fs";
949
- import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
950
- import path20 from "node:path";
1103
+ import { existsSync as existsSync10 } from "node:fs";
1104
+ import { mkdir as mkdir6, readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
1105
+ import path26 from "node:path";
1106
+
1107
+ // src/core/approval-replay.ts
1108
+ import path5 from "node:path";
1109
+
1110
+ // src/core/fingerprint.ts
1111
+ import { createHash } from "node:crypto";
1112
+ function canonicalStringify(value) {
1113
+ if (value === null || typeof value !== "object") {
1114
+ return JSON.stringify(value);
1115
+ }
1116
+ if (Array.isArray(value)) {
1117
+ return `[${value.map((item) => canonicalStringify(item)).join(",")}]`;
1118
+ }
1119
+ const entries = Object.entries(value).sort(
1120
+ ([left], [right]) => left.localeCompare(right)
1121
+ );
1122
+ return `{${entries.map(([key, child]) => `${JSON.stringify(key)}:${canonicalStringify(child)}`).join(",")}}`;
1123
+ }
1124
+ function hashValue(value) {
1125
+ return createHash("sha256").update(value).digest("hex");
1126
+ }
1127
+ function subagentFingerprint(kind, scrubbed, repoRoot) {
1128
+ return hashValue(`subagent:${kind}:${canonicalStringify(scrubbed)}:${repoRoot}`);
1129
+ }
1130
+ function toolFingerprint(toolName, scrubbed, repoRoot) {
1131
+ return hashValue(`tool:${toolName}:${canonicalStringify(scrubbed)}:${repoRoot}`);
1132
+ }
1133
+
1134
+ // src/core/approval-replay-cli.ts
1135
+ import { spawn } from "node:child_process";
1136
+ import path4 from "node:path";
1137
+ var DEFAULT_CLI_REPLAY_TIMEOUT_MS = 3e5;
1138
+ async function replayShellCommand(command, cwd, timeoutMs = DEFAULT_CLI_REPLAY_TIMEOUT_MS) {
1139
+ const resolvedCwd = path4.resolve(cwd);
1140
+ return new Promise((resolve, reject) => {
1141
+ const child = spawn(command, {
1142
+ cwd: resolvedCwd,
1143
+ shell: true,
1144
+ stdio: ["ignore", "pipe", "pipe"],
1145
+ env: { ...process.env }
1146
+ });
1147
+ const stdout = [];
1148
+ const stderr = [];
1149
+ let timedOut = false;
1150
+ const timer = setTimeout(() => {
1151
+ timedOut = true;
1152
+ child.kill("SIGTERM");
1153
+ }, timeoutMs);
1154
+ child.stdout?.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
1155
+ child.stderr?.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
1156
+ child.on("error", (error) => {
1157
+ clearTimeout(timer);
1158
+ reject(error);
1159
+ });
1160
+ child.on("close", (code) => {
1161
+ clearTimeout(timer);
1162
+ resolve({
1163
+ exitCode: timedOut ? 124 : code ?? 1,
1164
+ stdout: Buffer.concat(stdout).toString("utf8"),
1165
+ stderr: Buffer.concat(stderr).toString("utf8"),
1166
+ ...timedOut ? { timedOut: true } : {}
1167
+ });
1168
+ });
1169
+ });
1170
+ }
1171
+
1172
+ // src/core/approval-replay.ts
1173
+ var MAX_PAYLOAD_JSON_BYTES = 16384;
1174
+ function replayPayloadHash(kind, payload, repoRoot) {
1175
+ if (!payload || Object.keys(payload).length === 0) {
1176
+ return void 0;
1177
+ }
1178
+ const json = canonicalStringify(payload);
1179
+ return hashValue(`payload:${kind}:${json}:${repoRoot}`);
1180
+ }
1181
+ var DEFAULT_REPLAY_CAPABILITIES = {
1182
+ shell: true,
1183
+ tool: false,
1184
+ subagent: false
1185
+ };
1186
+ var ADAPTER_REPLAY_CAPABLE = {
1187
+ cursor: { ...DEFAULT_REPLAY_CAPABILITIES },
1188
+ claude: { ...DEFAULT_REPLAY_CAPABILITIES },
1189
+ codex: { ...DEFAULT_REPLAY_CAPABILITIES }
1190
+ };
1191
+ function getExecutionLeaseMs(config) {
1192
+ const ms = config.approval?.executionLeaseMs;
1193
+ return typeof ms === "number" && ms > 0 ? ms : 6e4;
1194
+ }
1195
+ function approvalFlow(config) {
1196
+ return config.approval?.flow === "two_step" ? "two_step" : "one_step";
1197
+ }
1198
+ function buildReplayEnvelopeFields(input) {
1199
+ const fields = {};
1200
+ const cmd = input.command ?? input.input ?? "";
1201
+ if (cmd) {
1202
+ fields.input = cmd;
1203
+ fields.inputKind = input.inputKind ?? input.kind;
1204
+ }
1205
+ if (input.cwd) {
1206
+ fields.cwd = path5.resolve(input.cwd);
1207
+ }
1208
+ if (input.toolName) {
1209
+ fields.toolName = input.toolName;
1210
+ }
1211
+ if (input.payload && Object.keys(input.payload).length > 0) {
1212
+ const json = canonicalStringify(input.payload);
1213
+ if (Buffer.byteLength(json, "utf8") <= MAX_PAYLOAD_JSON_BYTES) {
1214
+ fields.payloadJson = json;
1215
+ }
1216
+ fields.payloadHash = replayPayloadHash(input.kind, input.payload, input.repoRoot);
1217
+ }
1218
+ return fields;
1219
+ }
1220
+ function hasReplayEnvelope(approval) {
1221
+ return Boolean(approval.cwd || approval.toolName || approval.payloadHash);
1222
+ }
1223
+ function validateReplayEnvelope(approval, action) {
1224
+ if (!hasReplayEnvelope(approval)) {
1225
+ return true;
1226
+ }
1227
+ if (approval.kind !== action.kind) {
1228
+ return false;
1229
+ }
1230
+ if (approval.fingerprint !== action.fingerprint) {
1231
+ return false;
1232
+ }
1233
+ if (approval.repoRoot !== action.repoRoot) {
1234
+ return false;
1235
+ }
1236
+ if (approval.cwd && path5.resolve(approval.cwd) !== path5.resolve(action.cwd ?? "")) {
1237
+ return false;
1238
+ }
1239
+ if (approval.toolName && approval.toolName !== action.toolName) {
1240
+ return false;
1241
+ }
1242
+ if (approval.payloadHash) {
1243
+ const hash = replayPayloadHash(action.kind, action.payload, action.repoRoot);
1244
+ if (!hash || approval.payloadHash !== hash) {
1245
+ return false;
1246
+ }
1247
+ }
1248
+ return true;
1249
+ }
1250
+ function canAutoReplay(config, kind, adapter) {
1251
+ if (config.approvalSigning?.required) {
1252
+ return false;
1253
+ }
1254
+ if (approvalFlow(config) !== "one_step") {
1255
+ return false;
1256
+ }
1257
+ if (kind !== "shell" && kind !== "tool" && kind !== "subagent") {
1258
+ return false;
1259
+ }
1260
+ const scopes = config.approval?.autoReplayScopes;
1261
+ if (kind === "shell" && scopes?.shell === false) {
1262
+ return false;
1263
+ }
1264
+ if (kind === "tool" && scopes?.tool !== true) {
1265
+ return false;
1266
+ }
1267
+ if (kind === "subagent" && scopes?.subagent !== true) {
1268
+ return false;
1269
+ }
1270
+ if (adapter) {
1271
+ return ADAPTER_REPLAY_CAPABLE[adapter][kind] === true;
1272
+ }
1273
+ return kind === "shell" && scopes?.shell !== false;
1274
+ }
1275
+ function buildRetryInstructionForConfig(config, tokenPrefix, approvalId) {
1276
+ if (approvalFlow(config) === "one_step") {
1277
+ return `To allow this action once, send ${tokenPrefix} ${approvalId}. After approval, retry the same action unchanged.`;
1278
+ }
1279
+ return `To allow the next matching action once, send ${tokenPrefix} ${approvalId} and then retry the original action unchanged.`;
1280
+ }
1281
+ function buildApprovalRecordedMessage(config, approval, adapter) {
1282
+ if (!canAutoReplay(config, approval.kind, adapter)) {
1283
+ return `Belay approval recorded for ${approval.approvalId}. Retry the original action once before it expires.`;
1284
+ }
1285
+ if (approval.kind === "shell" && approval.input) {
1286
+ return `Belay approval recorded for ${approval.approvalId}. Retry this shell command unchanged: ${approval.input}`;
1287
+ }
1288
+ return `Belay approval recorded for ${approval.approvalId}. Retry the original action once before it expires.`;
1289
+ }
1290
+ function buildReplayHint(config, approval, adapter) {
1291
+ if (approvalFlow(config) !== "one_step") {
1292
+ return null;
1293
+ }
1294
+ const input = approval.input ?? approval.summary;
1295
+ if (!input) {
1296
+ return null;
1297
+ }
1298
+ const autoReplay = canAutoReplay(config, approval.kind, adapter);
1299
+ return {
1300
+ kind: approval.kind,
1301
+ input,
1302
+ cwd: approval.cwd,
1303
+ toolName: approval.toolName,
1304
+ fingerprint: approval.fingerprint,
1305
+ approvalId: approval.approvalId,
1306
+ autoReplay,
1307
+ fallbackToTwoStep: !autoReplay
1308
+ };
1309
+ }
951
1310
 
952
1311
  // src/core/approval.ts
953
- var APPROVAL_EXECUTION_LEASE_MS = 6e4;
1312
+ var APPROVAL_EXECUTION_LEASE_MS = DEFAULT_APPROVAL_CONFIG.executionLeaseMs;
954
1313
  function nowIso() {
955
1314
  return (/* @__PURE__ */ new Date()).toISOString();
956
1315
  }
@@ -987,8 +1346,34 @@ function approvalCommandMatch(prompt, tokenPrefix) {
987
1346
  }
988
1347
  return null;
989
1348
  }
990
- function buildRetryInstruction(tokenPrefix, approvalId) {
991
- return `To allow the next matching action once, send ${tokenPrefix} ${approvalId} and then retry the original action unchanged.`;
1349
+ function createApprovalRecordWithEnvelope(params) {
1350
+ const envelope = buildReplayEnvelopeFields({
1351
+ kind: params.kind,
1352
+ command: params.approvalInput?.input,
1353
+ input: params.approvalInput?.input,
1354
+ inputKind: params.approvalInput?.inputKind,
1355
+ cwd: params.approvalInput?.cwd,
1356
+ toolName: params.approvalInput?.toolName,
1357
+ payload: params.approvalInput?.payload,
1358
+ fingerprint: params.fingerprint,
1359
+ repoRoot: params.repoRoot
1360
+ });
1361
+ return createApprovalRecord({
1362
+ kind: params.kind,
1363
+ fingerprint: params.fingerprint,
1364
+ repoRoot: params.repoRoot,
1365
+ reason: params.reason,
1366
+ summary: params.summary,
1367
+ approvalTtlMinutes: params.approvalTtlMinutes,
1368
+ approvalId: params.approvalId,
1369
+ input: envelope.input ?? params.approvalInput?.input,
1370
+ inputKind: envelope.inputKind ?? params.approvalInput?.inputKind,
1371
+ cwd: envelope.cwd,
1372
+ toolName: envelope.toolName ?? params.approvalInput?.toolName,
1373
+ payloadHash: envelope.payloadHash,
1374
+ payloadJson: envelope.payloadJson,
1375
+ scopeHint: params.scopeHint
1376
+ });
992
1377
  }
993
1378
  function createApprovalRecord(params) {
994
1379
  const createdAt = nowIso();
@@ -1007,221 +1392,515 @@ function createApprovalRecord(params) {
1007
1392
  record.input = params.input;
1008
1393
  record.inputKind = params.inputKind ?? params.kind;
1009
1394
  }
1010
- return record;
1011
- }
1012
-
1013
- // src/core/approval-token.ts
1014
- import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
1015
- import { existsSync } from "node:fs";
1016
- import { mkdir, readFile, writeFile } from "node:fs/promises";
1017
- import path4 from "node:path";
1018
- function base64UrlEncode(value) {
1019
- return Buffer.from(value, "utf8").toString("base64url");
1020
- }
1021
- function base64UrlDecode(value) {
1022
- return Buffer.from(value, "base64url").toString("utf8");
1023
- }
1024
- function approvalSigningKeyPath(controlPlaneDir = defaultControlPlaneDir()) {
1025
- return path4.join(controlPlaneDir, "approval-signing.key");
1026
- }
1027
- async function loadOrCreateApprovalSigningKey(controlPlaneDir = defaultControlPlaneDir()) {
1028
- const keyPath = approvalSigningKeyPath(controlPlaneDir);
1029
- if (existsSync(keyPath)) {
1030
- return readFile(keyPath);
1395
+ if (params.cwd) {
1396
+ record.cwd = params.cwd;
1031
1397
  }
1032
- await mkdir(controlPlaneDir, { recursive: true });
1033
- const key = randomBytes(32);
1034
- await writeFile(keyPath, key, { mode: 384 });
1035
- return key;
1036
- }
1037
- function signPayload(payload, key) {
1038
- const body = base64UrlEncode(JSON.stringify(payload));
1039
- const signature = createHmac("sha256", key).update(body).digest("base64url");
1040
- return `${body}.${signature}`;
1041
- }
1042
- async function issueApprovalToken(payload, controlPlaneDir = defaultControlPlaneDir()) {
1043
- const key = await loadOrCreateApprovalSigningKey(controlPlaneDir);
1044
- return signPayload(payload, key);
1045
- }
1046
- async function verifyApprovalToken(token, controlPlaneDir = defaultControlPlaneDir()) {
1047
- const [body, signature] = token.split(".");
1048
- if (!body || !signature) {
1049
- return null;
1398
+ if (params.toolName) {
1399
+ record.toolName = params.toolName;
1050
1400
  }
1051
- const keyPath = approvalSigningKeyPath(controlPlaneDir);
1052
- if (!existsSync(keyPath)) {
1053
- return null;
1401
+ if (params.payloadHash) {
1402
+ record.payloadHash = params.payloadHash;
1054
1403
  }
1055
- const key = await readFile(keyPath);
1056
- const expected = createHmac("sha256", key).update(body).digest("base64url");
1057
- const actualBuffer = Buffer.from(signature);
1058
- const expectedBuffer = Buffer.from(expected);
1059
- if (actualBuffer.length !== expectedBuffer.length || !timingSafeEqual(actualBuffer, expectedBuffer)) {
1060
- return null;
1404
+ if (params.payloadJson) {
1405
+ record.payloadJson = params.payloadJson;
1061
1406
  }
1062
- try {
1063
- const payload = JSON.parse(base64UrlDecode(body));
1064
- if (Date.parse(payload.expiresAt) <= Date.now()) {
1065
- return null;
1066
- }
1067
- return payload;
1068
- } catch {
1069
- return null;
1407
+ if (params.scopeHint) {
1408
+ record.scopeHint = params.scopeHint;
1070
1409
  }
1410
+ return record;
1071
1411
  }
1072
1412
 
1073
- // src/core/approval-service.ts
1074
- async function recordApproval(params) {
1075
- const { approvalId, config, store, token, requireSignedToken = false } = params;
1076
- const pending = await store.loadPending();
1077
- pending.state = compactApprovals(pending.state);
1078
- const index = pending.state.approvals.findIndex((approval2) => approval2.approvalId === approvalId);
1079
- if (index === -1) {
1080
- await store.writePending(pending.filePath, pending.state);
1081
- return { ok: false, message: "Belay approval not found or expired." };
1082
- }
1083
- const [approval] = pending.state.approvals.slice(index, index + 1);
1084
- if (requireSignedToken) {
1085
- if (!token) {
1086
- return { ok: false, message: "Signed approval token required for out-of-band approval." };
1087
- }
1088
- const controlPlaneDir = configuredControlPlaneDir(config);
1089
- const verified = await verifyApprovalToken(token, controlPlaneDir);
1090
- if (!verified || verified.approvalId !== approvalId) {
1091
- return { ok: false, message: "Invalid or expired signed approval token." };
1092
- }
1093
- if (verified.fingerprint !== approval.fingerprint || verified.repoRoot !== approval.repoRoot) {
1094
- return { ok: false, message: "Signed approval token does not match the pending approval." };
1095
- }
1096
- }
1097
- pending.state.approvals.splice(index, 1);
1098
- await store.writePending(pending.filePath, pending.state);
1099
- const approved = await store.loadApproved();
1100
- approved.state = compactApprovals(approved.state);
1101
- approved.state.approvals.push({
1102
- ...approval,
1103
- approvedAt: (/* @__PURE__ */ new Date()).toISOString()
1104
- });
1105
- await store.writeApproved(approved.filePath, approved.state);
1106
- return {
1107
- ok: true,
1108
- message: `Belay approval recorded for ${approvalId}. Retry the original action once before it expires.`,
1109
- approval
1110
- };
1111
- }
1112
-
1113
- // src/core/capability/allowlist.ts
1114
- import { existsSync as existsSync3, readFileSync } from "node:fs";
1413
+ // src/adapters/layouts/codex.ts
1115
1414
  import path6 from "node:path";
1116
-
1117
- // src/core/path-utils.ts
1118
- import { existsSync as existsSync2, realpathSync } from "node:fs";
1119
- import path5 from "node:path";
1120
- function canonicalPath(targetPath) {
1121
- const resolved = path5.resolve(targetPath);
1122
- if (!resolved) {
1123
- return resolved;
1124
- }
1125
- const parsed = path5.parse(resolved);
1126
- let current = parsed.root;
1127
- const relativeParts = path5.relative(parsed.root || ".", resolved).split(path5.sep).filter(Boolean);
1128
- for (let i = 0; i < relativeParts.length; i++) {
1129
- const segment = relativeParts[i];
1130
- if (!segment) {
1131
- continue;
1132
- }
1133
- const candidate = current === "" ? segment : path5.join(current, segment);
1134
- if (!existsSync2(candidate)) {
1135
- return path5.join(candidate, ...relativeParts.slice(i + 1));
1136
- }
1137
- try {
1138
- current = realpathSync.native(candidate);
1139
- } catch {
1140
- return path5.join(candidate, ...relativeParts.slice(i + 1));
1141
- }
1142
- }
1143
- return current;
1415
+ function runnerCommand2(platform, repoRoot, hookName, ...args) {
1416
+ const hooksDir = path6.join(path6.resolve(repoRoot), ".codex", "hooks");
1417
+ return buildRunnerInvocation(platform, hooksDir, repoRoot, hookName, ...args);
1144
1418
  }
1145
- function pathWithinRoot(root, targetPath) {
1146
- const resolvedRoot = canonicalPath(root);
1147
- const resolvedTarget = canonicalPath(targetPath);
1148
- const relativePath = path5.relative(resolvedRoot, resolvedTarget);
1149
- if (relativePath === "") {
1150
- return true;
1419
+ var codexLayout = {
1420
+ name: "codex",
1421
+ configPath(repoRoot) {
1422
+ return path6.join(repoRoot, ".codex", "belay.config.json");
1423
+ },
1424
+ // Codex reads lifecycle hooks from `.codex/config.toml` (project layer).
1425
+ hooksSettingsPath(repoRoot) {
1426
+ return path6.join(repoRoot, ".codex", "config.toml");
1427
+ },
1428
+ hooksDir(repoRoot) {
1429
+ return path6.join(repoRoot, ".codex", "hooks");
1430
+ },
1431
+ runtimeDir(repoRoot) {
1432
+ return path6.join(repoRoot, ".codex", "belay", "runtime");
1433
+ },
1434
+ repoLocalStateDir(repoRoot) {
1435
+ return path6.join(repoRoot, ".codex", "belay");
1436
+ },
1437
+ defaultAuditLogPath(_repoRoot) {
1438
+ return path6.join(".codex", "belay", "audit.ndjson");
1439
+ },
1440
+ repoRootMarkers: [".git", ".codex"],
1441
+ runnerCommand: runnerCommand2,
1442
+ defaultConfig(repoRoot) {
1443
+ return {
1444
+ ...DEFAULT_CONFIG_V4,
1445
+ adapter: "codex",
1446
+ audit: {
1447
+ ...DEFAULT_CONFIG_V4.audit,
1448
+ logPath: codexLayout.defaultAuditLogPath(repoRoot)
1449
+ }
1450
+ };
1151
1451
  }
1152
- return !relativePath.startsWith("..") && !path5.isAbsolute(relativePath);
1452
+ };
1453
+
1454
+ // src/adapters/layouts/cursor.ts
1455
+ import path7 from "node:path";
1456
+ function runnerCommand3(platform, repoRoot, hookName, ...args) {
1457
+ const hooksDir = path7.join(path7.resolve(repoRoot), ".cursor", "hooks");
1458
+ return buildRunnerInvocation(platform, hooksDir, repoRoot, hookName, ...args);
1153
1459
  }
1154
- function relativeWithinRepo(repoRoot, targetPath) {
1155
- const resolvedRoot = canonicalPath(repoRoot);
1156
- const resolvedTarget = canonicalPath(targetPath);
1157
- const relativePath = path5.relative(resolvedRoot, resolvedTarget);
1158
- if (relativePath === "") {
1159
- return ".";
1160
- }
1161
- if (relativePath.startsWith("..")) {
1162
- return null;
1460
+ var cursorLayout = {
1461
+ name: "cursor",
1462
+ configPath(repoRoot) {
1463
+ return path7.join(repoRoot, ".cursor", "belay.config.json");
1464
+ },
1465
+ hooksSettingsPath(repoRoot) {
1466
+ return path7.join(repoRoot, ".cursor", "hooks.json");
1467
+ },
1468
+ hooksDir(repoRoot) {
1469
+ return path7.join(repoRoot, ".cursor", "hooks");
1470
+ },
1471
+ runtimeDir(repoRoot) {
1472
+ return path7.join(repoRoot, ".cursor", "belay", "runtime");
1473
+ },
1474
+ repoLocalStateDir(repoRoot) {
1475
+ return path7.join(repoRoot, ".cursor", "belay");
1476
+ },
1477
+ defaultAuditLogPath(_repoRoot) {
1478
+ return path7.join(".cursor", "belay", "audit.ndjson");
1479
+ },
1480
+ repoRootMarkers: [".git", ".cursor"],
1481
+ runnerCommand: runnerCommand3,
1482
+ defaultConfig(repoRoot) {
1483
+ return {
1484
+ ...DEFAULT_CONFIG_V4,
1485
+ adapter: "cursor",
1486
+ audit: {
1487
+ ...DEFAULT_CONFIG_V4.audit,
1488
+ logPath: cursorLayout.defaultAuditLogPath(repoRoot)
1489
+ }
1490
+ };
1163
1491
  }
1164
- return relativePath;
1492
+ };
1493
+
1494
+ // src/adapters/layouts/index.ts
1495
+ var layouts = {
1496
+ cursor: cursorLayout,
1497
+ claude: claudeLayout,
1498
+ codex: codexLayout
1499
+ };
1500
+ function getAdapterLayout(name = "cursor") {
1501
+ return layouts[name];
1165
1502
  }
1166
- function resolveMutationTarget(token, cwd) {
1167
- if (!token || token === "--" || token.startsWith("-")) {
1168
- return null;
1169
- }
1170
- if (token === "2>" || token === "1>" || token === "&>" || token === "1>>" || token === "2>>") {
1171
- return null;
1172
- }
1173
- if (path5.isAbsolute(token)) {
1174
- return canonicalPath(token);
1175
- }
1176
- if (token.startsWith("./") || token.startsWith("../")) {
1177
- return canonicalPath(path5.resolve(cwd, token));
1178
- }
1179
- if (!token.includes("/") && !token.includes("\\")) {
1180
- return canonicalPath(path5.resolve(cwd, token));
1503
+
1504
+ // src/core/config-layers.ts
1505
+ import path8 from "node:path";
1506
+
1507
+ // src/presets.ts
1508
+ var CONFIG_PRESETS = {
1509
+ strict: {
1510
+ mode: "enforce",
1511
+ policy: {
1512
+ ...DEFAULT_CONFIG_V3.policy,
1513
+ unknownLocalEffect: "deny",
1514
+ unparseableShell: "deny",
1515
+ confidenceThresholds: { allow: 0.9, flag: 0.8 },
1516
+ modelAssist: { enabled: false }
1517
+ },
1518
+ sandbox: { ...DEFAULT_CONFIG_V3.sandbox }
1519
+ },
1520
+ standard: {
1521
+ mode: "enforce"
1522
+ },
1523
+ "audit-first": {
1524
+ mode: "audit",
1525
+ policy: {
1526
+ ...DEFAULT_CONFIG_V3.policy,
1527
+ unknownLocalEffect: "deny",
1528
+ unparseableShell: "deny",
1529
+ confidenceThresholds: { allow: 0.88, flag: 0.72 },
1530
+ modelAssist: { enabled: false }
1531
+ },
1532
+ sandbox: { ...DEFAULT_CONFIG_V3.sandbox }
1533
+ },
1534
+ "l1-full-recommended": {
1535
+ mode: "enforce",
1536
+ policy: {
1537
+ ...DEFAULT_CONFIG_V3.policy,
1538
+ confidenceThresholds: { ...DEFAULT_CONFIG_V3.policy.confidenceThresholds },
1539
+ modelAssist: { ...DEFAULT_CONFIG_V3.policy.modelAssist }
1540
+ },
1541
+ sandbox: {
1542
+ enabled: true,
1543
+ runtime: "container",
1544
+ denyNetworkByDefault: true
1545
+ },
1546
+ egress: {
1547
+ ...DEFAULT_CONFIG_V3.egress,
1548
+ enabled: true,
1549
+ demoteL3External: true
1550
+ },
1551
+ approvalSigning: {
1552
+ required: true
1553
+ },
1554
+ controlPlane: {
1555
+ ...DEFAULT_CONFIG_V3.controlPlane,
1556
+ isolation: {
1557
+ mode: "separate-user",
1558
+ verifyAgentWritable: true
1559
+ }
1560
+ }
1181
1561
  }
1182
- return canonicalPath(path5.resolve(cwd, token));
1562
+ };
1563
+ function applyConfigPreset(preset, extra = {}) {
1564
+ const base = CONFIG_PRESETS[preset] ?? CONFIG_PRESETS.standard;
1565
+ return {
1566
+ version: 3,
1567
+ ...base,
1568
+ ...extra,
1569
+ policy: {
1570
+ ...DEFAULT_CONFIG_V3.policy,
1571
+ ...base.policy ?? {},
1572
+ ...extra.policy
1573
+ },
1574
+ sandbox: {
1575
+ ...DEFAULT_CONFIG_V3.sandbox,
1576
+ ...base.sandbox ?? {},
1577
+ ...extra.sandbox
1578
+ },
1579
+ egress: {
1580
+ ...DEFAULT_CONFIG_V3.egress,
1581
+ ...base.egress ?? {},
1582
+ ...extra.egress
1583
+ },
1584
+ approvalSigning: {
1585
+ ...DEFAULT_CONFIG_V3.approvalSigning,
1586
+ ...base.approvalSigning ?? {},
1587
+ ...extra.approvalSigning
1588
+ },
1589
+ controlPlane: {
1590
+ ...DEFAULT_CONFIG_V3.controlPlane,
1591
+ ...base.controlPlane ?? {},
1592
+ ...extra.controlPlane,
1593
+ isolation: {
1594
+ ...DEFAULT_CONFIG_V3.controlPlane.isolation,
1595
+ ...base.controlPlane?.isolation ?? {},
1596
+ ...extra.controlPlane?.isolation ?? {}
1597
+ }
1598
+ }
1599
+ };
1183
1600
  }
1184
1601
 
1185
- // src/core/capability/allowlist.ts
1186
- function fsScopeAllowlistPath(config, repoLocalStateDir) {
1187
- return path6.join(belayStateDir(config, repoLocalStateDir), "fs-scope-allowlist.json");
1602
+ // src/core/config-layers.ts
1603
+ function teamConfigPath(homedir = () => process.env.HOME ?? process.env.USERPROFILE ?? "") {
1604
+ const xdg = process.env.XDG_CONFIG_HOME?.trim();
1605
+ const base = xdg || path8.join(homedir(), ".config");
1606
+ return path8.join(base, "agent-belay", "team.config.json");
1188
1607
  }
1189
- function loadFsScopeAllowlistSync(filePath) {
1190
- if (!existsSync3(filePath)) {
1191
- return { version: 1, paths: [] };
1608
+ function applyProtectedLayer(config, builtin) {
1609
+ const controlPlane = { ...config.controlPlane };
1610
+ if (builtin.controlPlane.enabled && controlPlane.enabled === false) {
1611
+ controlPlane.enabled = true;
1612
+ }
1613
+ if (builtin.controlPlane.integrity === "hash-pinned" && controlPlane.integrity === "none") {
1614
+ controlPlane.integrity = "hash-pinned";
1192
1615
  }
1193
- const raw = JSON.parse(readFileSync(filePath, "utf8"));
1194
1616
  return {
1195
- version: 1,
1196
- paths: Array.isArray(raw.paths) ? raw.paths : []
1617
+ ...config,
1618
+ controlPlane
1197
1619
  };
1198
1620
  }
1199
- function normalizeAllowlistPath(targetPath) {
1200
- return canonicalPath(targetPath);
1621
+ function asV3Layer(raw) {
1622
+ if (!raw || typeof raw !== "object") {
1623
+ return { version: 3 };
1624
+ }
1625
+ return { version: 3, ...raw };
1201
1626
  }
1202
- function isPathAllowlisted(absolutePath, allowlist) {
1203
- const resolved = normalizeAllowlistPath(absolutePath);
1204
- return allowlist.paths.some((entry) => {
1205
- const scope = normalizeAllowlistPath(entry.path);
1206
- return resolved === scope || pathWithinRoot(scope, resolved);
1207
- });
1627
+ function mergeConfigLayer(base, layer) {
1628
+ const merged = mergeConfig(layer, base);
1629
+ if (!layer.policy) {
1630
+ return { ...merged, policy: base.policy };
1631
+ }
1632
+ return merged;
1208
1633
  }
1209
- function allPathsAllowlisted(absolutePaths, allowlist) {
1210
- return absolutePaths.length > 0 && absolutePaths.every((entry) => isPathAllowlisted(entry, allowlist));
1634
+ function resolveLayeredConfig(params) {
1635
+ const provenance = [{ path: "(builtin)", source: "builtin" }];
1636
+ let config = mergeConfig({}, params.adapterDefaults);
1637
+ if (params.teamConfig) {
1638
+ const teamFile = params.teamConfig;
1639
+ const teamRaw = teamFile.preset ? applyConfigPreset(teamFile.preset, teamFile.config ?? {}) : teamFile.config ?? params.teamConfig;
1640
+ rejectTeamLayerJudgeSecrets(
1641
+ teamRaw.judge,
1642
+ "team"
1643
+ );
1644
+ config = mergeConfigLayer(config, asV3Layer(teamRaw));
1645
+ provenance.push({
1646
+ path: params.teamConfigPath ?? teamConfigPath(),
1647
+ source: "team"
1648
+ });
1649
+ }
1650
+ config = mergeConfigLayer(config, asV3Layer(params.repoConfig));
1651
+ if (params.repoConfigPath) {
1652
+ provenance.push({ path: params.repoConfigPath, source: "repo" });
1653
+ }
1654
+ const protectedConfig = applyProtectedLayer(config, DEFAULT_CONFIG_V3);
1655
+ if (JSON.stringify(protectedConfig) !== JSON.stringify(config)) {
1656
+ provenance.push({ path: "(protected-layer)", source: "protected" });
1657
+ config = protectedConfig;
1658
+ }
1659
+ return { config, provenance };
1211
1660
  }
1212
1661
 
1213
- // src/core/capability/broker.ts
1214
- function hasSandboxRuntime(config) {
1215
- return config.sandbox.enabled && config.sandbox.runtime !== "none";
1662
+ // src/config-io.ts
1663
+ function resolveAdapterName(config) {
1664
+ if (config.adapter === "claude") {
1665
+ return "claude";
1666
+ }
1667
+ if (config.adapter === "codex") {
1668
+ return "codex";
1669
+ }
1670
+ return "cursor";
1216
1671
  }
1217
- function isCapabilityBrokerDemotionActive(config) {
1218
- return hasSandboxRuntime(config);
1672
+ function repoLocalStateDirFor(repoRoot, config) {
1673
+ return getAdapterLayout(resolveAdapterName(config)).repoLocalStateDir(repoRoot);
1219
1674
  }
1220
1675
 
1221
- // src/core/capability/paths.ts
1222
- import path7 from "node:path";
1676
+ // src/core/approval-token.ts
1677
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
1678
+ import { existsSync } from "node:fs";
1679
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
1680
+ import path9 from "node:path";
1681
+ function base64UrlEncode(value) {
1682
+ return Buffer.from(value, "utf8").toString("base64url");
1683
+ }
1684
+ function base64UrlDecode(value) {
1685
+ return Buffer.from(value, "base64url").toString("utf8");
1686
+ }
1687
+ function approvalSigningKeyPath(controlPlaneDir = defaultControlPlaneDir()) {
1688
+ return path9.join(controlPlaneDir, "approval-signing.key");
1689
+ }
1690
+ async function loadOrCreateApprovalSigningKey(controlPlaneDir = defaultControlPlaneDir()) {
1691
+ const keyPath = approvalSigningKeyPath(controlPlaneDir);
1692
+ if (existsSync(keyPath)) {
1693
+ return readFile(keyPath);
1694
+ }
1695
+ await mkdir(controlPlaneDir, { recursive: true });
1696
+ const key = randomBytes(32);
1697
+ await writeFile(keyPath, key, { mode: 384 });
1698
+ return key;
1699
+ }
1700
+ function signPayload(payload, key) {
1701
+ const body = base64UrlEncode(JSON.stringify(payload));
1702
+ const signature = createHmac("sha256", key).update(body).digest("base64url");
1703
+ return `${body}.${signature}`;
1704
+ }
1705
+ async function issueApprovalToken(payload, controlPlaneDir = defaultControlPlaneDir()) {
1706
+ const key = await loadOrCreateApprovalSigningKey(controlPlaneDir);
1707
+ return signPayload(payload, key);
1708
+ }
1709
+ async function verifyApprovalToken(token, controlPlaneDir = defaultControlPlaneDir()) {
1710
+ const [body, signature] = token.split(".");
1711
+ if (!body || !signature) {
1712
+ return null;
1713
+ }
1714
+ const keyPath = approvalSigningKeyPath(controlPlaneDir);
1715
+ if (!existsSync(keyPath)) {
1716
+ return null;
1717
+ }
1718
+ const key = await readFile(keyPath);
1719
+ const expected = createHmac("sha256", key).update(body).digest("base64url");
1720
+ const actualBuffer = Buffer.from(signature);
1721
+ const expectedBuffer = Buffer.from(expected);
1722
+ if (actualBuffer.length !== expectedBuffer.length || !timingSafeEqual(actualBuffer, expectedBuffer)) {
1723
+ return null;
1724
+ }
1725
+ try {
1726
+ const payload = JSON.parse(base64UrlDecode(body));
1727
+ if (Date.parse(payload.expiresAt) <= Date.now()) {
1728
+ return null;
1729
+ }
1730
+ return payload;
1731
+ } catch {
1732
+ return null;
1733
+ }
1734
+ }
1735
+
1736
+ // src/core/approval-service.ts
1737
+ async function recordApproval(params) {
1738
+ const { approvalId, config, store, token, requireSignedToken = false, adapter } = params;
1739
+ const pending = await store.loadPending();
1740
+ pending.state = compactApprovals(pending.state);
1741
+ const index = pending.state.approvals.findIndex((approval2) => approval2.approvalId === approvalId);
1742
+ if (index === -1) {
1743
+ await store.writePending(pending.filePath, pending.state);
1744
+ return { ok: false, message: "Belay approval not found or expired." };
1745
+ }
1746
+ const [approval] = pending.state.approvals.slice(index, index + 1);
1747
+ if (requireSignedToken) {
1748
+ if (!token) {
1749
+ return { ok: false, message: "Signed approval token required for out-of-band approval." };
1750
+ }
1751
+ const controlPlaneDir = configuredControlPlaneDir(config);
1752
+ const verified = await verifyApprovalToken(token, controlPlaneDir);
1753
+ if (!verified || verified.approvalId !== approvalId) {
1754
+ return { ok: false, message: "Invalid or expired signed approval token." };
1755
+ }
1756
+ if (verified.fingerprint !== approval.fingerprint || verified.repoRoot !== approval.repoRoot) {
1757
+ return { ok: false, message: "Signed approval token does not match the pending approval." };
1758
+ }
1759
+ }
1760
+ pending.state.approvals.splice(index, 1);
1761
+ await store.writePending(pending.filePath, pending.state);
1762
+ const approved = await store.loadApproved();
1763
+ approved.state = compactApprovals(approved.state);
1764
+ approved.state.approvals.push({
1765
+ ...approval,
1766
+ approvedAt: (/* @__PURE__ */ new Date()).toISOString()
1767
+ });
1768
+ await store.writeApproved(approved.filePath, approved.state);
1769
+ return {
1770
+ ok: true,
1771
+ message: buildApprovalRecordedMessage(config, approval, adapter),
1772
+ approval
1773
+ };
1774
+ }
1775
+ async function consumeApprovedAfterCliReplay(params) {
1776
+ const approved = await params.store.loadApproved();
1777
+ approved.state = compactApprovals(approved.state);
1778
+ const remaining = approved.state.approvals.filter(
1779
+ (approval) => approval.approvalId !== params.approvalId
1780
+ );
1781
+ if (remaining.length === approved.state.approvals.length) {
1782
+ return;
1783
+ }
1784
+ approved.state.approvals = remaining;
1785
+ await params.store.writeApproved(approved.filePath, approved.state);
1786
+ }
1787
+ function gateApprovalStoreFromDeps(deps) {
1788
+ return {
1789
+ loadPending: () => deps.loadApprovals("pending-approvals.json"),
1790
+ loadApproved: () => deps.loadApprovals("approved-approvals.json"),
1791
+ writePending: (filePath, state) => deps.writeApprovals(filePath, state),
1792
+ writeApproved: (filePath, state) => deps.writeApprovals(filePath, state)
1793
+ };
1794
+ }
1795
+
1796
+ // src/core/audit-replay-context.ts
1797
+ import { createHash as createHash2 } from "node:crypto";
1798
+ function hashReplayPayload(payload) {
1799
+ return createHash2("sha256").update(JSON.stringify(payload)).digest("hex");
1800
+ }
1801
+ function buildAuditActionSnapshot(kind, result, replayAction) {
1802
+ if (!replayAction?.cwd) {
1803
+ return void 0;
1804
+ }
1805
+ const replayKind = replayAction.kind;
1806
+ const resolvedKind = replayKind === "shell" || replayKind === "tool" || replayKind === "subagent" ? replayKind : kind;
1807
+ const normalizedAction = result.normalizedCommand ?? replayAction.command ?? result.summary ?? "";
1808
+ if (!normalizedAction.trim()) {
1809
+ return void 0;
1810
+ }
1811
+ return {
1812
+ schemaVersion: 1,
1813
+ kind: resolvedKind,
1814
+ cwd: replayAction.cwd,
1815
+ normalizedAction,
1816
+ ...replayAction.toolName ? { toolName: replayAction.toolName } : {},
1817
+ ...replayAction.payload ? { payloadHash: hashReplayPayload(replayAction.payload) } : {}
1818
+ };
1819
+ }
1820
+ function buildAuditReplayContext(kind, result, replayAction) {
1821
+ if (!replayAction?.cwd) {
1822
+ return void 0;
1823
+ }
1824
+ const replayKind = replayAction.kind;
1825
+ const resolvedKind = replayKind === "shell" || replayKind === "tool" || replayKind === "subagent" ? replayKind : kind;
1826
+ return {
1827
+ cwd: replayAction.cwd,
1828
+ kind: resolvedKind,
1829
+ command: replayAction.command ?? result.normalizedCommand ?? result.summary,
1830
+ ...replayAction.toolName ? { toolName: replayAction.toolName } : {},
1831
+ ...replayAction.payload ? { payload: replayAction.payload } : {}
1832
+ };
1833
+ }
1834
+
1835
+ // src/core/capability/allowlist.ts
1836
+ import { existsSync as existsSync3, readFileSync } from "node:fs";
1837
+ import path11 from "node:path";
1838
+
1839
+ // src/core/path-utils.ts
1840
+ import { existsSync as existsSync2, realpathSync, statSync } from "node:fs";
1841
+ import path10 from "node:path";
1223
1842
 
1224
1843
  // src/core/shell-tokenizer.ts
1844
+ var FD_DUPLICATION_PATTERN = /^\d+[<>]&(?:\d+|-)$/;
1845
+ var FD_REDIRECT_PATTERN = /^\d+(?:>>?|<)$/;
1846
+ function readDigits(input, index) {
1847
+ let end = index;
1848
+ while (end < input.length && /[0-9]/.test(input[end] ?? "")) {
1849
+ end += 1;
1850
+ }
1851
+ return input.slice(index, end);
1852
+ }
1853
+ function readShellOperator(input, index) {
1854
+ const char = input[index];
1855
+ const next = input[index + 1] ?? "";
1856
+ const digits = /[0-9]/.test(char) ? readDigits(input, index) : "";
1857
+ const digitsLength = digits.length;
1858
+ const afterDigits = digitsLength > 0 ? input[index + digitsLength] ?? "" : "";
1859
+ const afterDigitsNext = digitsLength > 0 ? input[index + digitsLength + 1] ?? "" : "";
1860
+ if (char === "&" && next === "&") {
1861
+ return { token: "&&", length: 2 };
1862
+ }
1863
+ if (char === "|" && next === "|") {
1864
+ return { token: "||", length: 2 };
1865
+ }
1866
+ if (char === "|" && next === "&") {
1867
+ return { token: "|&", length: 2 };
1868
+ }
1869
+ if (char === "&" && next === ">") {
1870
+ return { token: "&>", length: 2 };
1871
+ }
1872
+ if (digitsLength > 0 && (afterDigits === ">" || afterDigits === "<")) {
1873
+ if (afterDigitsNext === "&") {
1874
+ let end = index + digitsLength + 2;
1875
+ while (end < input.length && /[0-9-]/.test(input[end] ?? "")) {
1876
+ end += 1;
1877
+ }
1878
+ if (end > index + digitsLength + 2) {
1879
+ return { token: input.slice(index, end), length: end - index };
1880
+ }
1881
+ }
1882
+ if (afterDigits === ">" && afterDigitsNext === ">") {
1883
+ return { token: `${digits}>>`, length: digitsLength + 2 };
1884
+ }
1885
+ return { token: `${digits}${afterDigits}`, length: digitsLength + 1 };
1886
+ }
1887
+ if (char === ">" && next === ">") {
1888
+ return { token: ">>", length: 2 };
1889
+ }
1890
+ if (char === "&") {
1891
+ return { token: "&", length: 1 };
1892
+ }
1893
+ if (char === "|" || char === ";" || char === ">" || char === "<") {
1894
+ return { token: char, length: 1 };
1895
+ }
1896
+ return null;
1897
+ }
1898
+ function isRedirectOperator(token) {
1899
+ return token === ">" || token === ">>" || token === "<" || token === "&>" || FD_REDIRECT_PATTERN.test(token);
1900
+ }
1901
+ function isFdDuplication(token) {
1902
+ return FD_DUPLICATION_PATTERN.test(token);
1903
+ }
1225
1904
  function tokenizeShell(input) {
1226
1905
  const tokens = [];
1227
1906
  let buffer = "";
@@ -1235,7 +1914,6 @@ function tokenizeShell(input) {
1235
1914
  };
1236
1915
  for (let index = 0; index < input.length; index += 1) {
1237
1916
  const char = input[index];
1238
- const next = input[index + 1] ?? "";
1239
1917
  if (escaping) {
1240
1918
  buffer += char;
1241
1919
  escaping = false;
@@ -1257,27 +1935,11 @@ function tokenizeShell(input) {
1257
1935
  quote = char;
1258
1936
  continue;
1259
1937
  }
1260
- if (char === "&" && next === "&") {
1261
- flush();
1262
- tokens.push("&&");
1263
- index += 1;
1264
- continue;
1265
- }
1266
- if (char === "|" && next === "|") {
1267
- flush();
1268
- tokens.push("||");
1269
- index += 1;
1270
- continue;
1271
- }
1272
- if (char === ">" && next === ">") {
1273
- flush();
1274
- tokens.push(">>");
1275
- index += 1;
1276
- continue;
1277
- }
1278
- if (char === "|" || char === ";" || char === ">" || char === "<") {
1938
+ const operator = readShellOperator(input, index);
1939
+ if (operator) {
1279
1940
  flush();
1280
- tokens.push(char);
1941
+ tokens.push(operator.token);
1942
+ index += operator.length - 1;
1281
1943
  continue;
1282
1944
  }
1283
1945
  if (char === "\n" || char === "\r") {
@@ -1307,7 +1969,10 @@ function extractRedirectTargets(tokens) {
1307
1969
  const targets = [];
1308
1970
  for (let index = 0; index < tokens.length; index += 1) {
1309
1971
  const token = tokens[index];
1310
- if (token === ">" || token === ">>" || token === "<") {
1972
+ if (isFdDuplication(token)) {
1973
+ continue;
1974
+ }
1975
+ if (isRedirectOperator(token)) {
1311
1976
  const next = tokens[index + 1];
1312
1977
  if (next) {
1313
1978
  targets.push(next);
@@ -1317,73 +1982,215 @@ function extractRedirectTargets(tokens) {
1317
1982
  return targets;
1318
1983
  }
1319
1984
 
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]);
1985
+ // src/core/path-utils.ts
1986
+ function canonicalPath(targetPath) {
1987
+ const resolved = path10.resolve(targetPath);
1988
+ if (!resolved) {
1989
+ return resolved;
1990
+ }
1991
+ const parsed = path10.parse(resolved);
1992
+ let current = parsed.root;
1993
+ const relativeParts = path10.relative(parsed.root || ".", resolved).split(path10.sep).filter(Boolean);
1994
+ for (let i = 0; i < relativeParts.length; i++) {
1995
+ const segment = relativeParts[i];
1996
+ if (!segment) {
1327
1997
  continue;
1328
1998
  }
1329
- const moveMatch = line.match(/^\*\*\* Move to: (.+)$/);
1330
- if (moveMatch?.[1]) {
1331
- targets.push(moveMatch[1]);
1999
+ const candidate = current === "" ? segment : path10.join(current, segment);
2000
+ if (!existsSync2(candidate)) {
2001
+ return path10.join(candidate, ...relativeParts.slice(i + 1));
2002
+ }
2003
+ try {
2004
+ current = realpathSync.native(candidate);
2005
+ } catch {
2006
+ return path10.join(candidate, ...relativeParts.slice(i + 1));
1332
2007
  }
1333
2008
  }
1334
- return targets;
2009
+ return current;
1335
2010
  }
1336
- function extractToolFilePath(payload) {
1337
- const toolInput = payload.tool_input;
1338
- if (!toolInput || typeof toolInput !== "object") {
2011
+ function pathWithinRoot(root, targetPath) {
2012
+ const resolvedRoot = canonicalPath(root);
2013
+ const resolvedTarget = canonicalPath(targetPath);
2014
+ const relativePath = path10.relative(resolvedRoot, resolvedTarget);
2015
+ if (relativePath === "") {
2016
+ return true;
2017
+ }
2018
+ return !relativePath.startsWith("..") && !path10.isAbsolute(relativePath);
2019
+ }
2020
+ function relativeWithinRoot(root, targetPath, options = {}) {
2021
+ const resolvedRoot = options.canonicalizeRoot === false ? path10.resolve(root) : canonicalPath(root);
2022
+ const resolvedTarget = canonicalPath(targetPath);
2023
+ const relativePath = path10.relative(resolvedRoot, resolvedTarget);
2024
+ if (relativePath === "") {
2025
+ return ".";
2026
+ }
2027
+ if (relativePath.startsWith("..") || path10.isAbsolute(relativePath)) {
1339
2028
  return null;
1340
2029
  }
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];
2030
+ return relativePath;
2031
+ }
2032
+ function relativeWithinRepo(repoRoot, targetPath) {
2033
+ return relativeWithinRoot(repoRoot, targetPath);
2034
+ }
2035
+ function resolveWorkspaceRootMatch(repoRoot, trustedRoots = [], targetPath) {
2036
+ const canonicalRepoRoot = canonicalPath(repoRoot);
2037
+ const repoRelative = relativeWithinRoot(repoRoot, targetPath);
2038
+ if (repoRelative !== null) {
2039
+ return { kind: "repo", root: canonicalRepoRoot, relativePath: repoRelative };
2040
+ }
2041
+ const normalizedTrustedRoots = [...new Set(trustedRoots.map((root) => path10.resolve(root)))].filter((root) => root !== canonicalRepoRoot).sort((left, right) => right.length - left.length);
2042
+ for (const root of normalizedTrustedRoots) {
2043
+ const trustedRelative = relativeWithinRoot(root, targetPath, { canonicalizeRoot: false });
2044
+ if (trustedRelative !== null) {
2045
+ return { kind: "trusted", root, relativePath: trustedRelative };
1345
2046
  }
1346
2047
  }
1347
2048
  return null;
1348
2049
  }
1349
- function extractToolPatch(payload) {
1350
- const toolInput = payload.tool_input;
1351
- if (!toolInput || typeof toolInput !== "object") {
2050
+ function resolveMutationTarget(token, cwd) {
2051
+ if (!token || token === "--" || token.startsWith("-")) {
1352
2052
  return null;
1353
2053
  }
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
- }
2054
+ if (isRedirectOperator(token) || isFdDuplication(token) || token === "&&" || token === "||" || token === "|" || token === "|&" || token === ";" || token === "&") {
2055
+ return null;
1359
2056
  }
1360
- return null;
2057
+ if (path10.isAbsolute(token)) {
2058
+ return canonicalPath(token);
2059
+ }
2060
+ if (token.startsWith("./") || token.startsWith("../")) {
2061
+ return canonicalPath(path10.resolve(cwd, token));
2062
+ }
2063
+ if (!token.includes("/") && !token.includes("\\")) {
2064
+ return canonicalPath(path10.resolve(cwd, token));
2065
+ }
2066
+ return canonicalPath(path10.resolve(cwd, token));
2067
+ }
2068
+ function containingGitRoot(targetPath) {
2069
+ const resolved = canonicalPath(targetPath);
2070
+ let current = resolved;
2071
+ try {
2072
+ if (!statSync(current).isDirectory()) {
2073
+ current = path10.dirname(current);
2074
+ }
2075
+ } catch {
2076
+ current = path10.dirname(current);
2077
+ }
2078
+ while (true) {
2079
+ if (existsSync2(path10.join(current, ".git"))) {
2080
+ return current;
2081
+ }
2082
+ const parent = path10.dirname(current);
2083
+ if (parent === current) {
2084
+ return null;
2085
+ }
2086
+ current = parent;
2087
+ }
2088
+ }
2089
+
2090
+ // src/core/capability/allowlist.ts
2091
+ function fsScopeAllowlistPath(config, repoLocalStateDir) {
2092
+ return path11.join(belayStateDir(config, repoLocalStateDir), "fs-scope-allowlist.json");
2093
+ }
2094
+ function loadFsScopeAllowlistSync(filePath) {
2095
+ if (!existsSync3(filePath)) {
2096
+ return { version: 1, paths: [] };
2097
+ }
2098
+ const raw = JSON.parse(readFileSync(filePath, "utf8"));
2099
+ return {
2100
+ version: 1,
2101
+ paths: Array.isArray(raw.paths) ? raw.paths : []
2102
+ };
2103
+ }
2104
+ function normalizeAllowlistPath(targetPath) {
2105
+ return canonicalPath(targetPath);
2106
+ }
2107
+ function isPathAllowlisted(absolutePath, allowlist) {
2108
+ const resolved = normalizeAllowlistPath(absolutePath);
2109
+ return allowlist.paths.some((entry) => {
2110
+ const scope = normalizeAllowlistPath(entry.path);
2111
+ return resolved === scope || pathWithinRoot(scope, resolved);
2112
+ });
2113
+ }
2114
+ function allPathsAllowlisted(absolutePaths, allowlist) {
2115
+ return absolutePaths.length > 0 && absolutePaths.every((entry) => isPathAllowlisted(entry, allowlist));
2116
+ }
2117
+
2118
+ // src/core/capability/broker.ts
2119
+ function hasSandboxRuntime(config) {
2120
+ return config.sandbox.enabled && config.sandbox.runtime !== "none";
2121
+ }
2122
+ function isCapabilityBrokerDemotionActive(config) {
2123
+ return hasSandboxRuntime(config);
2124
+ }
2125
+
2126
+ // src/core/capability/paths.ts
2127
+ import path12 from "node:path";
2128
+ function applyPatchTargets(patch) {
2129
+ const targets = [];
2130
+ for (const line of patch.split("\n")) {
2131
+ const match = line.match(/^\*\*\* (?:Add|Delete|Update) File: (.+)$/);
2132
+ if (match?.[1]) {
2133
+ targets.push(match[1]);
2134
+ continue;
2135
+ }
2136
+ const moveMatch = line.match(/^\*\*\* Move to: (.+)$/);
2137
+ if (moveMatch?.[1]) {
2138
+ targets.push(moveMatch[1]);
2139
+ }
2140
+ }
2141
+ return targets;
2142
+ }
2143
+ function extractToolFilePath(payload) {
2144
+ const toolInput = payload.tool_input;
2145
+ if (!toolInput || typeof toolInput !== "object") {
2146
+ return null;
2147
+ }
2148
+ const input = toolInput;
2149
+ for (const key of ["path", "file_path", "target_file", "filePath"]) {
2150
+ if (typeof input[key] === "string") {
2151
+ return input[key];
2152
+ }
2153
+ }
2154
+ return null;
2155
+ }
2156
+ function extractToolPatch(payload) {
2157
+ const toolInput = payload.tool_input;
2158
+ if (!toolInput || typeof toolInput !== "object") {
2159
+ return null;
2160
+ }
2161
+ const input = toolInput;
2162
+ for (const key of ["patch", "input", "text"]) {
2163
+ if (typeof input[key] === "string" && input[key].trim()) {
2164
+ return input[key];
2165
+ }
2166
+ }
2167
+ return null;
1361
2168
  }
1362
- function addOutsideRepoPath(paths, token, cwd, repoRoot) {
2169
+ function addOutsideRepoPath(paths, token, cwd, repoRoot, trustedWorkspaceRoots = []) {
1363
2170
  const resolved = resolveMutationTarget(token, cwd);
1364
- if (resolved && relativeWithinRepo(repoRoot, resolved) === null) {
2171
+ if (resolved && resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolved) === null) {
1365
2172
  paths.add(resolved);
1366
2173
  }
1367
2174
  }
1368
- function collectOutsideRepoPaths(command, cwd, repoRoot) {
2175
+ function collectOutsideRepoPaths(command, cwd, repoRoot, trustedWorkspaceRoots = []) {
1369
2176
  const tokens = tokenizeShell(command);
1370
2177
  const redirects = extractRedirectTargets(tokens);
1371
2178
  const paths = /* @__PURE__ */ new Set();
1372
2179
  for (const token of tokens.slice(1)) {
1373
- addOutsideRepoPath(paths, token, cwd, repoRoot);
2180
+ addOutsideRepoPath(paths, token, cwd, repoRoot, trustedWorkspaceRoots);
1374
2181
  }
1375
2182
  for (const redirect of redirects) {
1376
- addOutsideRepoPath(paths, redirect, cwd, repoRoot);
2183
+ addOutsideRepoPath(paths, redirect, cwd, repoRoot, trustedWorkspaceRoots);
1377
2184
  }
1378
2185
  return [...paths];
1379
2186
  }
1380
- function collectOutsideRepoPathsFromToolPayload(payload, cwd, repoRoot) {
2187
+ function collectOutsideRepoPathsFromToolPayload(payload, cwd, repoRoot, trustedWorkspaceRoots = []) {
1381
2188
  const toolKind = String(payload.tool_name ?? "").trim().toLowerCase();
1382
2189
  const paths = /* @__PURE__ */ new Set();
1383
2190
  const filePath = extractToolFilePath(payload);
1384
2191
  if (filePath) {
1385
- const resolved = path7.isAbsolute(filePath) ? filePath : path7.resolve(cwd, filePath);
1386
- if (relativeWithinRepo(repoRoot, resolved) === null) {
2192
+ const resolved = path12.isAbsolute(filePath) ? filePath : path12.resolve(cwd, filePath);
2193
+ if (resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolved) === null) {
1387
2194
  paths.add(resolved);
1388
2195
  }
1389
2196
  return [...paths];
@@ -1392,8 +2199,8 @@ function collectOutsideRepoPathsFromToolPayload(payload, cwd, repoRoot) {
1392
2199
  const patch = extractToolPatch(payload);
1393
2200
  if (patch) {
1394
2201
  for (const target of applyPatchTargets(patch)) {
1395
- const resolved = path7.isAbsolute(target) ? target : path7.resolve(cwd, target);
1396
- if (relativeWithinRepo(repoRoot, resolved) === null) {
2202
+ const resolved = path12.isAbsolute(target) ? target : path12.resolve(cwd, target);
2203
+ if (resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolved) === null) {
1397
2204
  paths.add(resolved);
1398
2205
  }
1399
2206
  }
@@ -1416,186 +2223,123 @@ function shouldSkipBrokerApprovedRecord(brokerActive, approvalReason) {
1416
2223
  return brokerActive && approvalReason !== void 0 && FS_SCOPE_REASONS.has(approvalReason);
1417
2224
  }
1418
2225
 
1419
- // src/core/config-layers.ts
1420
- import path8 from "node:path";
1421
-
1422
- // src/presets.ts
1423
- var CONFIG_PRESETS = {
1424
- strict: {
1425
- mode: "enforce",
1426
- policy: {
1427
- ...DEFAULT_CONFIG_V3.policy,
1428
- unknownLocalEffect: "deny",
1429
- unparseableShell: "deny",
1430
- confidenceThresholds: { allow: 0.9, flag: 0.8 },
1431
- modelAssist: { enabled: false }
1432
- },
1433
- sandbox: { ...DEFAULT_CONFIG_V3.sandbox }
1434
- },
1435
- standard: {
1436
- mode: "enforce"
1437
- },
1438
- "audit-first": {
1439
- mode: "audit",
1440
- policy: {
1441
- ...DEFAULT_CONFIG_V3.policy,
1442
- unknownLocalEffect: "deny",
1443
- unparseableShell: "deny",
1444
- confidenceThresholds: { allow: 0.88, flag: 0.72 },
1445
- modelAssist: { enabled: false }
1446
- },
1447
- sandbox: { ...DEFAULT_CONFIG_V3.sandbox }
1448
- },
1449
- "l1-full-recommended": {
1450
- mode: "enforce",
1451
- policy: {
1452
- ...DEFAULT_CONFIG_V3.policy,
1453
- confidenceThresholds: { ...DEFAULT_CONFIG_V3.policy.confidenceThresholds },
1454
- modelAssist: { ...DEFAULT_CONFIG_V3.policy.modelAssist }
1455
- },
1456
- sandbox: {
1457
- enabled: true,
1458
- runtime: "container",
1459
- denyNetworkByDefault: true
1460
- },
1461
- egress: {
1462
- ...DEFAULT_CONFIG_V3.egress,
1463
- enabled: true,
1464
- demoteL3External: true
1465
- },
1466
- approvalSigning: {
1467
- required: true
1468
- },
1469
- controlPlane: {
1470
- ...DEFAULT_CONFIG_V3.controlPlane,
1471
- isolation: {
1472
- mode: "separate-user",
1473
- verifyAgentWritable: true
1474
- }
1475
- }
2226
+ // src/core/capability/trusted-workspace-roots.ts
2227
+ import { existsSync as existsSync4, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
2228
+ import path13 from "node:path";
2229
+ function trustedWorkspaceRootsPath(config, repoLocalStateDir) {
2230
+ return path13.join(belayStateDir(config, repoLocalStateDir), "trusted-workspace-roots.json");
2231
+ }
2232
+ function loadTrustedWorkspaceRootsSync(filePath) {
2233
+ if (!existsSync4(filePath)) {
2234
+ return { version: 1, roots: [] };
1476
2235
  }
1477
- };
1478
- function applyConfigPreset(preset, extra = {}) {
1479
- const base = CONFIG_PRESETS[preset] ?? CONFIG_PRESETS.standard;
1480
- return {
1481
- version: 3,
1482
- ...base,
1483
- ...extra,
1484
- policy: {
1485
- ...DEFAULT_CONFIG_V3.policy,
1486
- ...base.policy ?? {},
1487
- ...extra.policy
1488
- },
1489
- sandbox: {
1490
- ...DEFAULT_CONFIG_V3.sandbox,
1491
- ...base.sandbox ?? {},
1492
- ...extra.sandbox
1493
- },
1494
- egress: {
1495
- ...DEFAULT_CONFIG_V3.egress,
1496
- ...base.egress ?? {},
1497
- ...extra.egress
1498
- },
1499
- approvalSigning: {
1500
- ...DEFAULT_CONFIG_V3.approvalSigning,
1501
- ...base.approvalSigning ?? {},
1502
- ...extra.approvalSigning
1503
- },
1504
- controlPlane: {
1505
- ...DEFAULT_CONFIG_V3.controlPlane,
1506
- ...base.controlPlane ?? {},
1507
- ...extra.controlPlane,
1508
- isolation: {
1509
- ...DEFAULT_CONFIG_V3.controlPlane.isolation,
1510
- ...base.controlPlane?.isolation ?? {},
1511
- ...extra.controlPlane?.isolation ?? {}
1512
- }
1513
- }
1514
- };
2236
+ const raw = JSON.parse(readFileSync2(filePath, "utf8"));
2237
+ return { version: 1, roots: sanitizeTrustedWorkspaceRootEntries(raw.roots) };
1515
2238
  }
1516
-
1517
- // src/core/config-layers.ts
1518
- function teamConfigPath(homedir = () => process.env.HOME ?? process.env.USERPROFILE ?? "") {
1519
- const xdg = process.env.XDG_CONFIG_HOME?.trim();
1520
- const base = xdg || path8.join(homedir(), ".config");
1521
- return path8.join(base, "agent-belay", "team.config.json");
2239
+ function normalizeTrustedWorkspaceRootPath(targetPath) {
2240
+ return canonicalPath(targetPath);
1522
2241
  }
1523
- function applyProtectedLayer(config, builtin) {
1524
- const controlPlane = { ...config.controlPlane };
1525
- if (builtin.controlPlane.enabled && controlPlane.enabled === false) {
1526
- controlPlane.enabled = true;
1527
- }
1528
- if (builtin.controlPlane.integrity === "hash-pinned" && controlPlane.integrity === "none") {
1529
- controlPlane.integrity = "hash-pinned";
2242
+ function sanitizeTrustedWorkspaceRootEntries(input) {
2243
+ if (!Array.isArray(input)) {
2244
+ return [];
1530
2245
  }
1531
- return {
1532
- ...config,
1533
- controlPlane
1534
- };
2246
+ return input.flatMap((entry) => {
2247
+ if (!entry || typeof entry !== "object") {
2248
+ return [];
2249
+ }
2250
+ const record = entry;
2251
+ if (typeof record.path !== "string" || !record.path.trim()) {
2252
+ return [];
2253
+ }
2254
+ const approvedAt = typeof record.approvedAt === "string" ? record.approvedAt : (/* @__PURE__ */ new Date(0)).toISOString();
2255
+ const approvalId = typeof record.approvalId === "string" ? record.approvalId : "unknown";
2256
+ const source = record.source === "approval" ? "approval" : void 0;
2257
+ return [
2258
+ {
2259
+ path: path13.resolve(record.path),
2260
+ approvedAt,
2261
+ approvalId,
2262
+ ...source ? { source } : {}
2263
+ }
2264
+ ];
2265
+ });
1535
2266
  }
1536
- function asV3Layer(raw) {
1537
- if (!raw || typeof raw !== "object") {
1538
- return { version: 3 };
2267
+ var SYSTEM_HIGH_STAKES_PREFIXES = [
2268
+ "/etc",
2269
+ "/private/etc",
2270
+ "/bin",
2271
+ "/sbin",
2272
+ "/usr",
2273
+ "/opt",
2274
+ "/System",
2275
+ "/Library"
2276
+ ];
2277
+ var HOME_HIGH_STAKES_SEGMENTS = [".ssh", ".gnupg", ".aws", ".kube", ".docker", ".config"];
2278
+ function isDirectoryPath(targetPath) {
2279
+ try {
2280
+ return statSync2(targetPath).isDirectory();
2281
+ } catch {
2282
+ return false;
1539
2283
  }
1540
- return { version: 3, ...raw };
1541
2284
  }
1542
- function mergeConfigLayer(base, layer) {
1543
- const merged = mergeConfig(layer, base);
1544
- if (!layer.policy) {
1545
- return { ...merged, policy: base.policy };
2285
+ function isBroadTrustedWorkspaceRoot(targetPath) {
2286
+ const home = process.env.HOME ?? process.env.USERPROFILE;
2287
+ const root = normalizeTrustedWorkspaceRootPath(targetPath);
2288
+ if (root === normalizeTrustedWorkspaceRootPath(path13.parse(root).root)) {
2289
+ return true;
1546
2290
  }
1547
- return merged;
2291
+ if (home && normalizeTrustedWorkspaceRootPath(home) === root) {
2292
+ return true;
2293
+ }
2294
+ return false;
1548
2295
  }
1549
- function resolveLayeredConfig(params) {
1550
- const provenance = [{ path: "(builtin)", source: "builtin" }];
1551
- let config = mergeConfig({}, params.adapterDefaults);
1552
- if (params.teamConfig) {
1553
- const teamFile = params.teamConfig;
1554
- const teamRaw = teamFile.preset ? applyConfigPreset(teamFile.preset, teamFile.config ?? {}) : teamFile.config ?? params.teamConfig;
1555
- rejectTeamLayerJudgeSecrets(
1556
- teamRaw.judge,
1557
- "team"
1558
- );
1559
- config = mergeConfigLayer(config, asV3Layer(teamRaw));
1560
- provenance.push({
1561
- path: params.teamConfigPath ?? teamConfigPath(),
1562
- source: "team"
1563
- });
2296
+ function isHighStakesTrustedWorkspaceRoot(targetPath) {
2297
+ const normalized = normalizeTrustedWorkspaceRootPath(targetPath);
2298
+ if (SYSTEM_HIGH_STAKES_PREFIXES.some(
2299
+ (prefix) => normalized === normalizeTrustedWorkspaceRootPath(prefix) || pathWithinRoot(normalizeTrustedWorkspaceRootPath(prefix), normalized)
2300
+ )) {
2301
+ return true;
1564
2302
  }
1565
- config = mergeConfigLayer(config, asV3Layer(params.repoConfig));
1566
- if (params.repoConfigPath) {
1567
- provenance.push({ path: params.repoConfigPath, source: "repo" });
2303
+ const home = process.env.HOME ?? process.env.USERPROFILE;
2304
+ if (!home) {
2305
+ return false;
1568
2306
  }
1569
- const protectedConfig = applyProtectedLayer(config, DEFAULT_CONFIG_V3);
1570
- if (JSON.stringify(protectedConfig) !== JSON.stringify(config)) {
1571
- provenance.push({ path: "(protected-layer)", source: "protected" });
1572
- config = protectedConfig;
2307
+ const homeRoot = normalizeTrustedWorkspaceRootPath(home);
2308
+ if (!pathWithinRoot(homeRoot, normalized)) {
2309
+ return false;
1573
2310
  }
1574
- return { config, provenance };
2311
+ return HOME_HIGH_STAKES_SEGMENTS.some(
2312
+ (segment) => pathWithinRoot(path13.join(homeRoot, segment), normalized)
2313
+ );
1575
2314
  }
1576
-
1577
- // src/core/fingerprint.ts
1578
- import { createHash } from "node:crypto";
1579
- function canonicalStringify(value) {
1580
- if (value === null || typeof value !== "object") {
1581
- return JSON.stringify(value);
2315
+ function validateTrustedWorkspaceRootCandidate(params) {
2316
+ const normalizedPath = normalizeTrustedWorkspaceRootPath(params.candidatePath);
2317
+ if (params.requireExistingDirectory !== false && !isDirectoryPath(normalizedPath)) {
2318
+ return { ok: false, normalizedPath, reason: "not_directory" };
1582
2319
  }
1583
- if (Array.isArray(value)) {
1584
- return `[${value.map((item) => canonicalStringify(item)).join(",")}]`;
2320
+ if (isBroadTrustedWorkspaceRoot(normalizedPath)) {
2321
+ return { ok: false, normalizedPath, reason: "broad_root" };
1585
2322
  }
1586
- const entries = Object.entries(value).sort(
1587
- ([left], [right]) => left.localeCompare(right)
1588
- );
1589
- return `{${entries.map(([key, child]) => `${JSON.stringify(key)}:${canonicalStringify(child)}`).join(",")}}`;
1590
- }
1591
- function hashValue(value) {
1592
- return createHash("sha256").update(value).digest("hex");
1593
- }
1594
- function subagentFingerprint(kind, scrubbed, repoRoot) {
1595
- return hashValue(`subagent:${kind}:${canonicalStringify(scrubbed)}:${repoRoot}`);
1596
- }
1597
- function toolFingerprint(toolName, scrubbed, repoRoot) {
1598
- return hashValue(`tool:${toolName}:${canonicalStringify(scrubbed)}:${repoRoot}`);
2323
+ if (isHighStakesTrustedWorkspaceRoot(normalizedPath)) {
2324
+ return { ok: false, normalizedPath, reason: "high_stakes" };
2325
+ }
2326
+ if (resolveWorkspaceRootMatch(params.repoRoot, [], normalizedPath) !== null) {
2327
+ return { ok: false, normalizedPath, reason: "inside_repo" };
2328
+ }
2329
+ if (params.requireNonGit !== false && containingGitRoot(normalizedPath)) {
2330
+ return { ok: false, normalizedPath, reason: "inside_git_repo" };
2331
+ }
2332
+ const normalizedControlPlane = params.controlPlaneDir?.trim() ? normalizeTrustedWorkspaceRootPath(params.controlPlaneDir) : null;
2333
+ if (normalizedControlPlane && (pathWithinRoot(normalizedControlPlane, normalizedPath) || pathWithinRoot(normalizedPath, normalizedControlPlane))) {
2334
+ return { ok: false, normalizedPath, reason: "control_plane_overlap" };
2335
+ }
2336
+ if ((params.protectedRoots ?? []).some((root) => {
2337
+ const normalizedRoot = normalizeTrustedWorkspaceRootPath(root);
2338
+ return pathWithinRoot(normalizedRoot, normalizedPath) || pathWithinRoot(normalizedPath, normalizedRoot);
2339
+ })) {
2340
+ return { ok: false, normalizedPath, reason: "protected_root_overlap" };
2341
+ }
2342
+ return { ok: true, normalizedPath };
1599
2343
  }
1600
2344
 
1601
2345
  // src/core/scrub.ts
@@ -1683,6 +2427,51 @@ function scrubValue(value, options = DEFAULT_SCRUB_OPTIONS) {
1683
2427
  return value;
1684
2428
  }
1685
2429
 
2430
+ // src/core/replay-scrub.ts
2431
+ function subagentFingerprintSource(payload, scrubOptions) {
2432
+ const toolInput = payload.tool_input;
2433
+ if (toolInput && typeof toolInput === "object") {
2434
+ const input = toolInput;
2435
+ return scrubValue(
2436
+ {
2437
+ description: input.description ?? "",
2438
+ prompt: input.prompt ?? ""
2439
+ },
2440
+ scrubOptions
2441
+ );
2442
+ }
2443
+ const task = payload.task;
2444
+ if (typeof task === "string") {
2445
+ return scrubValue({ task }, scrubOptions);
2446
+ }
2447
+ if (task && typeof task === "object") {
2448
+ const taskObj = task;
2449
+ return scrubValue(
2450
+ {
2451
+ description: taskObj.description ?? "",
2452
+ prompt: taskObj.prompt ?? ""
2453
+ },
2454
+ scrubOptions
2455
+ );
2456
+ }
2457
+ return scrubValue(payload, scrubOptions);
2458
+ }
2459
+ function fingerprintReplayPayload(kind, payload, scrubOptions) {
2460
+ if (!payload) {
2461
+ return void 0;
2462
+ }
2463
+ if (kind === "tool") {
2464
+ const toolInput = payload.tool_input;
2465
+ if (toolInput && typeof toolInput === "object") {
2466
+ return scrubValue(toolInput, scrubOptions);
2467
+ }
2468
+ }
2469
+ if (kind === "subagent") {
2470
+ return subagentFingerprintSource(payload, scrubOptions);
2471
+ }
2472
+ return scrubValue(payload, scrubOptions);
2473
+ }
2474
+
1686
2475
  // src/core/classify-subagent.ts
1687
2476
  var EXTERNAL_TERMS = ["deploy", "production", "publish", "release", "ship", "notify", "email"];
1688
2477
  function extractSubagentText(payload, options) {
@@ -1706,32 +2495,7 @@ function extractSubagentText(payload, options) {
1706
2495
  return canonicalStringify(scrubValue(payload, options.scrubOptions));
1707
2496
  }
1708
2497
  function fingerprintSource(payload, options) {
1709
- const toolInput = payload.tool_input;
1710
- if (toolInput && typeof toolInput === "object") {
1711
- const input = toolInput;
1712
- return scrubValue(
1713
- {
1714
- description: input.description ?? "",
1715
- prompt: input.prompt ?? ""
1716
- },
1717
- options.scrubOptions
1718
- );
1719
- }
1720
- const task = payload.task;
1721
- if (typeof task === "string") {
1722
- return scrubValue({ task }, options.scrubOptions);
1723
- }
1724
- if (task && typeof task === "object") {
1725
- const taskObj = task;
1726
- return scrubValue(
1727
- {
1728
- description: taskObj.description ?? "",
1729
- prompt: taskObj.prompt ?? ""
1730
- },
1731
- options.scrubOptions
1732
- );
1733
- }
1734
- return scrubValue(payload, options.scrubOptions);
2498
+ return subagentFingerprintSource(payload, options.scrubOptions ?? {});
1735
2499
  }
1736
2500
  function classifySubagent(payload, repoRoot, options = {}) {
1737
2501
  const kind = payload.tool_name === "Task" ? "Task" : String(payload.subagent_type ?? "generalPurpose");
@@ -1759,7 +2523,7 @@ function classifySubagent(payload, repoRoot, options = {}) {
1759
2523
  }
1760
2524
 
1761
2525
  // src/core/classify-tool.ts
1762
- import path16 from "node:path";
2526
+ import path21 from "node:path";
1763
2527
 
1764
2528
  // src/core/glob.ts
1765
2529
  function matchesSensitivePath(filePath, patterns) {
@@ -1805,6 +2569,10 @@ function matchesSensitivePath(filePath, patterns) {
1805
2569
  }
1806
2570
 
1807
2571
  // src/core/verdict/judge-audit.ts
2572
+ import { createHash as createHash3 } from "node:crypto";
2573
+ function hashJudgeSessionRef(value) {
2574
+ return createHash3("sha256").update(value).digest("hex").slice(0, 16);
2575
+ }
1808
2576
  function judgeTraceAuditFields(trace) {
1809
2577
  if (!trace) {
1810
2578
  return {};
@@ -1815,121 +2583,40 @@ function judgeTraceAuditFields(trace) {
1815
2583
  judgeModelResolved: trace.modelResolved,
1816
2584
  judgeLatencyMs: trace.latencyMs,
1817
2585
  ...trace.outboundRedacted !== void 0 ? { judgeOutboundRedacted: trace.outboundRedacted } : {},
1818
- ...trace.fallbackReason ? { judgeFallbackReason: trace.fallbackReason } : {}
2586
+ ...trace.judgeFallbackReason ? { judgeFallbackReason: trace.judgeFallbackReason } : trace.fallbackReason ? { judgeFallbackReason: trace.fallbackReason } : {},
2587
+ ...trace.judgeSessionUsed !== void 0 ? { judgeSessionUsed: trace.judgeSessionUsed } : {},
2588
+ ...trace.judgeSessionReused !== void 0 ? { judgeSessionReused: trace.judgeSessionReused } : {},
2589
+ ...trace.judgeSessionRefHash ? { judgeSessionRefHash: trace.judgeSessionRefHash } : {},
2590
+ ...trace.judgeSessionResetReason ? { judgeSessionResetReason: trace.judgeSessionResetReason } : {},
2591
+ ...trace.judgeConnectMs !== void 0 ? { judgeConnectMs: trace.judgeConnectMs } : {},
2592
+ ...trace.judgeEvalMs !== void 0 ? { judgeEvalMs: trace.judgeEvalMs } : {},
2593
+ ...trace.judgeParseMs !== void 0 ? { judgeParseMs: trace.judgeParseMs } : {},
2594
+ ...trace.judgeShadowCompared !== void 0 ? { judgeShadowCompared: trace.judgeShadowCompared } : {},
2595
+ ...trace.judgeShadowMismatch !== void 0 ? { judgeShadowMismatch: trace.judgeShadowMismatch } : {},
2596
+ ...trace.judgeShadowMismatchRateWindow !== void 0 ? { judgeShadowMismatchRateWindow: trace.judgeShadowMismatchRateWindow } : {},
2597
+ ...trace.judgeKillSwitchTriggered !== void 0 ? { judgeKillSwitchTriggered: trace.judgeKillSwitchTriggered } : {}
1819
2598
  };
1820
2599
  }
1821
2600
 
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
- };
1860
- }
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);
1868
- }
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];
1911
- }
1912
-
1913
- // src/config-io.ts
1914
- function resolveAdapterName(config) {
1915
- if (config.adapter === "claude") {
1916
- return "claude";
2601
+ // src/core/verdict/judge-baseline.ts
2602
+ var samples = [];
2603
+ var MAX_SAMPLES = 2e3;
2604
+ function recordJudgeLatency(phase, latencyMs, at = Date.now()) {
2605
+ if (!Number.isFinite(latencyMs) || latencyMs < 0) {
2606
+ return;
1917
2607
  }
1918
- if (config.adapter === "codex") {
1919
- return "codex";
2608
+ samples.push({ phase, latencyMs, at });
2609
+ if (samples.length > MAX_SAMPLES) {
2610
+ samples.splice(0, samples.length - MAX_SAMPLES);
1920
2611
  }
1921
- return "cursor";
1922
- }
1923
- function repoLocalStateDirFor(repoRoot, config) {
1924
- return getAdapterLayout(resolveAdapterName(config)).repoLocalStateDir(repoRoot);
1925
2612
  }
1926
2613
 
1927
2614
  // src/core/credential-store.ts
1928
2615
  import { chmod, mkdir as mkdir2, readFile as readFile2, unlink, writeFile as writeFile2 } from "node:fs/promises";
1929
- import path11 from "node:path";
2616
+ import path14 from "node:path";
1930
2617
  var STORE_FILENAME = "credentials.json";
1931
2618
  function credentialStorePath(stateDir) {
1932
- return path11.join(stateDir, STORE_FILENAME);
2619
+ return path14.join(stateDir, STORE_FILENAME);
1933
2620
  }
1934
2621
  async function readJudgeCredentialStore(stateDir) {
1935
2622
  const filePath = credentialStorePath(stateDir);
@@ -1954,7 +2641,7 @@ function parseCredentialRef(ref) {
1954
2641
 
1955
2642
  // src/core/judge-runtime-detection.ts
1956
2643
  import { accessSync, constants } from "node:fs";
1957
- import path12 from "node:path";
2644
+ import path15 from "node:path";
1958
2645
  var CLI_COMMANDS = {
1959
2646
  codex: "codex",
1960
2647
  cursor: "cursor-agent",
@@ -1971,8 +2658,8 @@ function cliTransportForProvider(providerId) {
1971
2658
  }
1972
2659
  function commandOnPath(command, env) {
1973
2660
  const pathValue = env.PATH ?? "";
1974
- for (const dir of pathValue.split(path12.delimiter).filter(Boolean)) {
1975
- const candidate = path12.join(dir, command);
2661
+ for (const dir of pathValue.split(path15.delimiter).filter(Boolean)) {
2662
+ const candidate = path15.join(dir, command);
1976
2663
  try {
1977
2664
  accessSync(candidate, constants.X_OK);
1978
2665
  return true;
@@ -2099,7 +2786,7 @@ function hasValidCloudConsent(judge) {
2099
2786
  }
2100
2787
 
2101
2788
  // src/core/verdict/containment.ts
2102
- import path13 from "node:path";
2789
+ import path16 from "node:path";
2103
2790
 
2104
2791
  // src/core/verdict/persistent-paths.ts
2105
2792
  function isPersistentAgentPath(resolvedPath) {
@@ -2118,7 +2805,7 @@ function expandHome(token) {
2118
2805
  if (!home) {
2119
2806
  return token;
2120
2807
  }
2121
- return token === "~" ? home : path13.join(home, token.slice(2));
2808
+ return token === "~" ? home : path16.join(home, token.slice(2));
2122
2809
  }
2123
2810
  return token;
2124
2811
  }
@@ -2130,16 +2817,16 @@ function resolveTrustedPath(token, trustedCwd, trusted) {
2130
2817
  return null;
2131
2818
  }
2132
2819
  const expanded = expandHome(token);
2133
- if (path13.isAbsolute(expanded)) {
2820
+ if (path16.isAbsolute(expanded)) {
2134
2821
  return canonicalPath(expanded);
2135
2822
  }
2136
- return canonicalPath(path13.resolve(trustedCwd, expanded));
2823
+ return canonicalPath(path16.resolve(trustedCwd, expanded));
2137
2824
  }
2138
- function locationForPath(resolvedPath, repoRoot) {
2825
+ function locationForPath(resolvedPath, repoRoot, trustedWorkspaceRoots = []) {
2139
2826
  if (!resolvedPath) {
2140
2827
  return "unknown";
2141
2828
  }
2142
- if (pathWithinRoot(repoRoot, resolvedPath)) {
2829
+ if (resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolvedPath)) {
2143
2830
  return "repo_local";
2144
2831
  }
2145
2832
  return "repo_outside";
@@ -2150,17 +2837,17 @@ function isGitPath(resolvedPath, repoRoot) {
2150
2837
  return false;
2151
2838
  }
2152
2839
  const normalized = relative.replaceAll("\\", "/");
2153
- return normalized === ".git" || normalized.startsWith(".git/");
2840
+ return normalized === ".git" || normalized.startsWith(".git/") || normalized.endsWith("/.git") || normalized.includes("/.git/");
2154
2841
  }
2155
- function isHighStakesPath(resolvedPath, repoRoot, sensitivePaths, protectedRoots = []) {
2842
+ function isHighStakesPath(resolvedPath, repoRoot, sensitivePaths, protectedRoots = [], trustedWorkspaceRoots = []) {
2156
2843
  if (isGitPath(resolvedPath, repoRoot)) {
2157
2844
  return true;
2158
2845
  }
2159
- const relative = relativeWithinRepo(repoRoot, resolvedPath);
2160
- if (relative !== null && matchesSensitivePath(relative.replaceAll("\\", "/"), sensitivePaths)) {
2846
+ const workspaceMatch = resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolvedPath);
2847
+ if (workspaceMatch !== null && matchesSensitivePath(workspaceMatch.relativePath.replaceAll("\\", "/"), sensitivePaths)) {
2161
2848
  return true;
2162
2849
  }
2163
- if (relative === null && isOutsideRepoSecretCredentialPath(resolvedPath)) {
2850
+ if ((workspaceMatch === null || workspaceMatch.kind === "trusted") && isOutsideRepoSecretCredentialPath(resolvedPath)) {
2164
2851
  return true;
2165
2852
  }
2166
2853
  return protectedRoots.some((root) => pathWithinRoot(root, resolvedPath));
@@ -2181,14 +2868,14 @@ function isDestructiveMutationHead(head) {
2181
2868
  function touchesProtectedRoot(resolvedPath, protectedRoots) {
2182
2869
  return protectedRoots.some((root) => pathWithinRoot(root, resolvedPath) || root === resolvedPath);
2183
2870
  }
2184
- function isDestructiveHighStakesMutation(head, resolvedPath, repoRoot, sensitivePaths, protectedRoots = []) {
2871
+ function isDestructiveHighStakesMutation(head, resolvedPath, repoRoot, sensitivePaths, protectedRoots = [], trustedWorkspaceRoots = []) {
2185
2872
  if (touchesProtectedRoot(resolvedPath, protectedRoots)) {
2186
2873
  return true;
2187
2874
  }
2188
2875
  if (!isDestructiveMutationHead(head)) {
2189
2876
  return false;
2190
2877
  }
2191
- return isHighStakesPath(resolvedPath, repoRoot, sensitivePaths, []);
2878
+ return isHighStakesPath(resolvedPath, repoRoot, sensitivePaths, [], trustedWorkspaceRoots);
2192
2879
  }
2193
2880
  function analyzePathTargets(params) {
2194
2881
  const signals = [];
@@ -2203,13 +2890,14 @@ function analyzePathTargets(params) {
2203
2890
  let isHighStakes = false;
2204
2891
  for (const target of params.targets) {
2205
2892
  const resolved = resolveTrustedPath(target, params.cwd, params.trustedCwd) ?? resolveMutationTarget(target, params.cwd);
2206
- const location2 = locationForPath(resolved, params.repoRoot);
2893
+ const location2 = locationForPath(resolved, params.repoRoot, params.trustedWorkspaceRoots);
2207
2894
  locations.add(location2);
2208
2895
  if (resolved && isHighStakesPath(
2209
2896
  resolved,
2210
2897
  params.repoRoot,
2211
2898
  params.sensitivePaths,
2212
- params.protectedArtifactRoots
2899
+ params.protectedArtifactRoots,
2900
+ params.trustedWorkspaceRoots
2213
2901
  )) {
2214
2902
  isHighStakes = true;
2215
2903
  signals.push("high_stakes_path");
@@ -2376,8 +3064,12 @@ function prescanMutationTargets(params) {
2376
3064
  if (!resolved) {
2377
3065
  continue;
2378
3066
  }
2379
- const relative = relativeWithinRepo(params.repoRoot, resolved);
2380
- if (relative !== null && matchesSensitivePath(relative.replaceAll("\\", "/"), params.sensitivePaths)) {
3067
+ const workspaceMatch = resolveWorkspaceRootMatch(
3068
+ params.repoRoot,
3069
+ params.trustedWorkspaceRoots,
3070
+ resolved
3071
+ );
3072
+ if (workspaceMatch !== null && matchesSensitivePath(workspaceMatch.relativePath.replaceAll("\\", "/"), params.sensitivePaths)) {
2381
3073
  return {
2382
3074
  local_recoverable: false,
2383
3075
  destroys_outside_repo: false,
@@ -2385,7 +3077,7 @@ function prescanMutationTargets(params) {
2385
3077
  reason: "sensitive_path_mutation"
2386
3078
  };
2387
3079
  }
2388
- if (relative === null && isOutsideRepoSecretCredentialPath(resolved)) {
3080
+ if ((workspaceMatch === null || workspaceMatch.kind === "trusted") && isOutsideRepoSecretCredentialPath(resolved)) {
2389
3081
  return {
2390
3082
  local_recoverable: false,
2391
3083
  destroys_outside_repo: false,
@@ -2605,19 +3297,1123 @@ function createOpenAiCompatibleJudge(options) {
2605
3297
  }
2606
3298
  }
2607
3299
  };
2608
- return judge;
2609
- }
2610
- function tier1RequiresAsk(verdict2) {
2611
- return !verdict2.local_recoverable || verdict2.destroys_history_or_secrets;
3300
+ return judge;
3301
+ }
3302
+ function tier1RequiresAsk(verdict2) {
3303
+ return !verdict2.local_recoverable || verdict2.destroys_history_or_secrets;
3304
+ }
3305
+
3306
+ // src/core/verdict/judge-cli.ts
3307
+ import { spawn as spawn4 } from "node:child_process";
3308
+
3309
+ // src/core/verdict/judge-session-guard.ts
3310
+ function buildJudgeSessionKey(parts) {
3311
+ return [parts.providerId, parts.model, parts.repoRoot, parts.judgeMode, parts.cliVersion].join(
3312
+ ":"
3313
+ );
3314
+ }
3315
+ function evaluateSessionReuse(existing, requested, budget, config, now = Date.now(), incomingPromptBytes = 0) {
3316
+ const sessionKey = buildJudgeSessionKey(requested);
3317
+ if (!config.enabled) {
3318
+ return { canReuse: false, resetReason: "session_disabled", sessionKey };
3319
+ }
3320
+ if (!config.providerAllowlist.includes(requested.providerId)) {
3321
+ return { canReuse: false, resetReason: "provider_not_allowlisted", sessionKey };
3322
+ }
3323
+ if (!existing || !budget) {
3324
+ return { canReuse: false, resetReason: "initial", sessionKey };
3325
+ }
3326
+ const existingKey = buildJudgeSessionKey(existing);
3327
+ if (existingKey !== sessionKey) {
3328
+ if (existing.providerId !== requested.providerId) {
3329
+ return { canReuse: false, resetReason: "provider_mismatch", sessionKey };
3330
+ }
3331
+ if (existing.model !== requested.model) {
3332
+ return { canReuse: false, resetReason: "model_mismatch", sessionKey };
3333
+ }
3334
+ if (existing.repoRoot !== requested.repoRoot) {
3335
+ return { canReuse: false, resetReason: "repo_mismatch", sessionKey };
3336
+ }
3337
+ if (existing.judgeMode !== requested.judgeMode) {
3338
+ return { canReuse: false, resetReason: "mode_mismatch", sessionKey };
3339
+ }
3340
+ if (existing.cliVersion !== requested.cliVersion) {
3341
+ return { canReuse: false, resetReason: "cli_version_mismatch", sessionKey };
3342
+ }
3343
+ return { canReuse: false, resetReason: "provider_mismatch", sessionKey };
3344
+ }
3345
+ if (budget.turnCount >= config.maxTurns) {
3346
+ return { canReuse: false, resetReason: "max_turns_exceeded", sessionKey };
3347
+ }
3348
+ if (now - budget.createdAtMs >= config.maxAgeMs) {
3349
+ return { canReuse: false, resetReason: "max_age_exceeded", sessionKey };
3350
+ }
3351
+ if (now - budget.lastUsedAtMs >= config.maxIdleMs) {
3352
+ return { canReuse: false, resetReason: "max_idle_exceeded", sessionKey };
3353
+ }
3354
+ if (budget.promptBytes + incomingPromptBytes > config.maxPromptBytes) {
3355
+ return { canReuse: false, resetReason: "max_prompt_bytes_exceeded", sessionKey };
3356
+ }
3357
+ return { canReuse: true, sessionKey };
3358
+ }
3359
+ function exceedsPromptBudget(promptBytes, config, existingBudget = null) {
3360
+ const total = (existingBudget?.promptBytes ?? 0) + promptBytes;
3361
+ return total > config.maxPromptBytes;
3362
+ }
3363
+ function guardFailClosedFallbackReason(resetReason) {
3364
+ switch (resetReason) {
3365
+ case "parse_failure":
3366
+ return "parse_error";
3367
+ case "non_json_response":
3368
+ return "non_json_response";
3369
+ case "cli_error":
3370
+ return "cli_error";
3371
+ case "timeout":
3372
+ return "timeout";
3373
+ case "cli_version_mismatch":
3374
+ return "version_mismatch";
3375
+ case "kill_switch":
3376
+ return "kill_switch";
3377
+ case "session_disabled":
3378
+ return "session_disabled";
3379
+ case "provider_not_allowlisted":
3380
+ return "provider_not_allowlisted";
3381
+ case "max_prompt_bytes_exceeded":
3382
+ return "guard_reset";
3383
+ default:
3384
+ return "guard_reset";
3385
+ }
3386
+ }
3387
+ function transportFallbackToFailClosedReason(providerId, fallback) {
3388
+ if (!fallback) {
3389
+ return `${providerId}_cli_parse_error`;
3390
+ }
3391
+ if (fallback.endsWith("_cli_unavailable") || fallback.endsWith("_cli_parse_error")) {
3392
+ return fallback;
3393
+ }
3394
+ switch (fallback) {
3395
+ case "non_json_response":
3396
+ case "parse_error":
3397
+ case "parse_timeout":
3398
+ return `${providerId}_cli_parse_error`;
3399
+ default:
3400
+ return `${providerId}_cli_unavailable`;
3401
+ }
3402
+ }
3403
+
3404
+ // src/core/verdict/judge-broker-service.ts
3405
+ import { spawn as spawn2 } from "node:child_process";
3406
+ import { existsSync as existsSync5 } from "node:fs";
3407
+ import { unlink as unlink2 } from "node:fs/promises";
3408
+ import net from "node:net";
3409
+ import path17 from "node:path";
3410
+ import { fileURLToPath } from "node:url";
3411
+
3412
+ // src/core/verdict/judge-session-mutex.ts
3413
+ var AsyncMutex = class {
3414
+ chain = Promise.resolve();
3415
+ run(fn) {
3416
+ const next = this.chain.then(fn);
3417
+ this.chain = next.then(
3418
+ () => void 0,
3419
+ () => void 0
3420
+ );
3421
+ return next;
3422
+ }
3423
+ };
3424
+ var MutexRegistry = class {
3425
+ mutexes = /* @__PURE__ */ new Map();
3426
+ forKey(key) {
3427
+ const existing = this.mutexes.get(key);
3428
+ if (existing) {
3429
+ return existing;
3430
+ }
3431
+ const created = new AsyncMutex();
3432
+ this.mutexes.set(key, created);
3433
+ return created;
3434
+ }
3435
+ clear() {
3436
+ this.mutexes.clear();
3437
+ }
3438
+ };
3439
+
3440
+ // src/core/verdict/judge-session-broker.ts
3441
+ var JudgeSessionBroker = class {
3442
+ sessions = /* @__PURE__ */ new Map();
3443
+ mutexes = new MutexRegistry();
3444
+ options;
3445
+ constructor(options) {
3446
+ this.options = options;
3447
+ }
3448
+ listSessions() {
3449
+ return [...this.sessions.values()];
3450
+ }
3451
+ invalidateSession(sessionKey, reason) {
3452
+ const session = this.sessions.get(sessionKey);
3453
+ if (session) {
3454
+ session.lastResetReason = reason;
3455
+ session.state = "idle";
3456
+ }
3457
+ this.sessions.delete(sessionKey);
3458
+ }
3459
+ stopAll(reason = "manual_stop") {
3460
+ const count = this.sessions.size;
3461
+ for (const session of this.sessions.values()) {
3462
+ session.lastResetReason = reason;
3463
+ session.state = "idle";
3464
+ }
3465
+ this.sessions.clear();
3466
+ this.mutexes.clear();
3467
+ return count;
3468
+ }
3469
+ async evaluate(request, timeoutMs) {
3470
+ const sessionKey = buildJudgeSessionKey(request.keyParts);
3471
+ return this.mutexes.forKey(sessionKey).run(() => this.evaluateExclusive(request, timeoutMs));
3472
+ }
3473
+ async evaluateExclusive(request, timeoutMs) {
3474
+ const sessionKey = buildJudgeSessionKey(request.keyParts);
3475
+ const providerId = request.keyParts.providerId;
3476
+ const existing = this.sessions.get(sessionKey);
3477
+ const reuseDecision = evaluateSessionReuse(
3478
+ existing?.keyParts ?? null,
3479
+ request.keyParts,
3480
+ existing?.budget ?? null,
3481
+ this.options.config,
3482
+ Date.now(),
3483
+ request.promptBytes
3484
+ );
3485
+ const resetReason = reuseDecision.canReuse ? void 0 : reuseDecision.resetReason;
3486
+ let session = existing;
3487
+ if (!reuseDecision.canReuse || !session) {
3488
+ session = {
3489
+ keyParts: request.keyParts,
3490
+ budget: {
3491
+ turnCount: 0,
3492
+ createdAtMs: Date.now(),
3493
+ lastUsedAtMs: Date.now(),
3494
+ promptBytes: 0
3495
+ },
3496
+ state: "busy",
3497
+ lastResetReason: resetReason ?? "initial"
3498
+ };
3499
+ this.enforceProviderSessionCap(providerId, sessionKey);
3500
+ this.sessions.set(sessionKey, session);
3501
+ } else {
3502
+ session.state = "busy";
3503
+ session.budget.lastUsedAtMs = Date.now();
3504
+ }
3505
+ const invocation = reuseDecision.canReuse && session.providerResumeId ? this.buildResumeInvocation(request.invocation, providerId, session.providerResumeId) : request.invocation;
3506
+ const resumed = reuseDecision.canReuse && Boolean(session.providerResumeId);
3507
+ try {
3508
+ const raw = await this.options.runCommand(invocation, timeoutMs);
3509
+ session.budget.turnCount += 1;
3510
+ session.budget.promptBytes += request.promptBytes;
3511
+ session.budget.lastUsedAtMs = Date.now();
3512
+ session.state = "idle";
3513
+ const extracted = this.options.extractResumeId?.(providerId, raw) ?? extractProviderResumeId(providerId, raw) ?? void 0;
3514
+ if (extracted) {
3515
+ session.providerResumeId = extracted;
3516
+ }
3517
+ return {
3518
+ raw,
3519
+ reused: resumed,
3520
+ resetReason: session.lastResetReason,
3521
+ providerResumeId: session.providerResumeId
3522
+ };
3523
+ } catch (error) {
3524
+ session.state = "idle";
3525
+ session.lastResetReason = "cli_error";
3526
+ this.sessions.delete(sessionKey);
3527
+ throw error;
3528
+ }
3529
+ }
3530
+ enforceProviderSessionCap(providerId, keepKey) {
3531
+ const cap = this.options.config.maxSessionsPerProvider;
3532
+ const sameProvider = [...this.sessions.entries()].filter(
3533
+ ([, record]) => record.keyParts.providerId === providerId
3534
+ );
3535
+ if (sameProvider.length < cap) {
3536
+ return;
3537
+ }
3538
+ for (const [key] of sameProvider) {
3539
+ if (key === keepKey) {
3540
+ continue;
3541
+ }
3542
+ this.sessions.delete(key);
3543
+ if (this.sessions.size < cap) {
3544
+ break;
3545
+ }
3546
+ }
3547
+ }
3548
+ buildResumeInvocation(base, providerId, resumeId) {
3549
+ if (providerId === "cursor") {
3550
+ const args = [...base.args];
3551
+ const modelIndex = args.indexOf("--model");
3552
+ const model = modelIndex >= 0 ? args[modelIndex + 1] : void 0;
3553
+ const prompt = args[args.length - 1];
3554
+ return {
3555
+ binary: base.binary,
3556
+ args: [
3557
+ "--print",
3558
+ "--output-format",
3559
+ "json",
3560
+ "--mode",
3561
+ "ask",
3562
+ "--sandbox",
3563
+ "enabled",
3564
+ "--trust",
3565
+ "--resume",
3566
+ resumeId,
3567
+ ...model ? ["--model", model] : [],
3568
+ prompt
3569
+ ]
3570
+ };
3571
+ }
3572
+ if (providerId === "claude") {
3573
+ return {
3574
+ ...base,
3575
+ args: [...base.args, "--continue", resumeId]
3576
+ };
3577
+ }
3578
+ return base;
3579
+ }
3580
+ };
3581
+ function extractProviderResumeId(providerId, raw) {
3582
+ try {
3583
+ const parsed = JSON.parse(raw);
3584
+ const candidates = [
3585
+ parsed.chat_id,
3586
+ parsed.chatId,
3587
+ parsed.session_id,
3588
+ parsed.sessionId,
3589
+ parsed.result?.chat_id,
3590
+ parsed.result?.session_id
3591
+ ];
3592
+ for (const candidate of candidates) {
3593
+ if (typeof candidate === "string" && candidate.trim()) {
3594
+ return candidate.trim();
3595
+ }
3596
+ }
3597
+ } catch {
3598
+ }
3599
+ if (providerId === "cursor") {
3600
+ const match = raw.match(/"chat_id"\s*:\s*"([^"]+)"/);
3601
+ if (match?.[1]) {
3602
+ return match[1];
3603
+ }
3604
+ }
3605
+ return null;
3606
+ }
3607
+ var repoBrokers = /* @__PURE__ */ new Map();
3608
+ function getRepoJudgeSessionBroker(repoRoot, options) {
3609
+ const existing = repoBrokers.get(repoRoot);
3610
+ if (existing) {
3611
+ return existing;
3612
+ }
3613
+ const broker = new JudgeSessionBroker(options);
3614
+ repoBrokers.set(repoRoot, broker);
3615
+ return broker;
3616
+ }
3617
+
3618
+ // src/core/verdict/judge-broker-service.ts
3619
+ var JUDGE_BROKER_SOCKET = "judge-broker.sock";
3620
+ var JUDGE_BROKER_STATUS = "judge-broker.json";
3621
+ var JUDGE_BROKER_PID = "judge-broker.pid";
3622
+ var JUDGE_BROKER_SESSION = "judge-broker-session.json";
3623
+ function judgeBrokerPaths(stateDir) {
3624
+ return {
3625
+ stateDir,
3626
+ socketPath: path17.join(stateDir, JUDGE_BROKER_SOCKET),
3627
+ statusPath: path17.join(stateDir, JUDGE_BROKER_STATUS),
3628
+ pidPath: path17.join(stateDir, JUDGE_BROKER_PID),
3629
+ sessionConfigPath: path17.join(stateDir, JUDGE_BROKER_SESSION)
3630
+ };
3631
+ }
3632
+ function daemonScriptPath() {
3633
+ return fileURLToPath(new URL("../../judge-broker-daemon.js", import.meta.url));
3634
+ }
3635
+ function isProcessAlive(pid) {
3636
+ try {
3637
+ process.kill(pid, 0);
3638
+ return true;
3639
+ } catch {
3640
+ return false;
3641
+ }
3642
+ }
3643
+ function useInProcessBroker(env = process.env) {
3644
+ return Boolean(env.VITEST || env.VITEST_WORKER_ID || env.BELAY_JUDGE_BROKER_IN_PROCESS === "1");
3645
+ }
3646
+ async function readBrokerStatus(statusPath) {
3647
+ if (!existsSync5(statusPath)) {
3648
+ return null;
3649
+ }
3650
+ try {
3651
+ const { readFile: readFile6 } = await import("node:fs/promises");
3652
+ const raw = JSON.parse(await readFile6(statusPath, "utf8"));
3653
+ if (typeof raw.pid !== "number" || typeof raw.socketPath !== "string") {
3654
+ return null;
3655
+ }
3656
+ return raw;
3657
+ } catch {
3658
+ return null;
3659
+ }
3660
+ }
3661
+ async function readBrokerSessionConfig(sessionConfigPath) {
3662
+ if (!existsSync5(sessionConfigPath)) {
3663
+ return null;
3664
+ }
3665
+ try {
3666
+ const { readFile: readFile6 } = await import("node:fs/promises");
3667
+ const raw = JSON.parse(
3668
+ await readFile6(sessionConfigPath, "utf8")
3669
+ );
3670
+ return normalizeJudgeSessionConfig({ ...raw, enabled: true });
3671
+ } catch {
3672
+ return null;
3673
+ }
3674
+ }
3675
+ function brokerSessionConfigPayload(sessionConfig) {
3676
+ return JSON.stringify(normalizeJudgeSessionConfig({ ...sessionConfig, enabled: true }));
3677
+ }
3678
+ async function writeBrokerSessionConfig(paths, sessionConfig) {
3679
+ const { writeFile: writeFile6 } = await import("node:fs/promises");
3680
+ await writeFile6(paths.sessionConfigPath, brokerSessionConfigPayload(sessionConfig), {
3681
+ encoding: "utf8",
3682
+ mode: 384
3683
+ });
3684
+ }
3685
+ async function connectBrokerSocket(socketPath, timeoutMs) {
3686
+ return new Promise((resolve, reject) => {
3687
+ const socket = net.createConnection(socketPath);
3688
+ const timer = setTimeout(() => {
3689
+ socket.destroy();
3690
+ reject(new Error("judge broker connect timed out"));
3691
+ }, timeoutMs);
3692
+ socket.once("connect", () => {
3693
+ clearTimeout(timer);
3694
+ resolve(socket);
3695
+ });
3696
+ socket.once("error", (error) => {
3697
+ clearTimeout(timer);
3698
+ reject(error);
3699
+ });
3700
+ });
3701
+ }
3702
+ async function rpcEvaluate(socketPath, request, timeoutMs, connectTimeoutMs) {
3703
+ const socket = await connectBrokerSocket(socketPath, connectTimeoutMs);
3704
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
3705
+ const payload = JSON.stringify({
3706
+ type: "evaluate",
3707
+ id,
3708
+ request,
3709
+ timeoutMs
3710
+ });
3711
+ return new Promise((resolve, reject) => {
3712
+ let buffer = "";
3713
+ const timer = setTimeout(() => {
3714
+ socket.destroy();
3715
+ reject(new Error("judge broker evaluate timed out"));
3716
+ }, timeoutMs + connectTimeoutMs);
3717
+ const onData = (chunk) => {
3718
+ buffer += String(chunk);
3719
+ const lines = buffer.split("\n");
3720
+ buffer = lines.pop() ?? "";
3721
+ for (const line of lines) {
3722
+ if (!line.trim()) {
3723
+ continue;
3724
+ }
3725
+ try {
3726
+ const message = JSON.parse(line);
3727
+ if (message.id !== id) {
3728
+ continue;
3729
+ }
3730
+ clearTimeout(timer);
3731
+ socket.destroy();
3732
+ if (message.type === "error") {
3733
+ reject(new Error(message.error ?? "judge broker error"));
3734
+ return;
3735
+ }
3736
+ if (message.result) {
3737
+ resolve(message.result);
3738
+ }
3739
+ } catch {
3740
+ }
3741
+ }
3742
+ };
3743
+ socket.on("data", onData);
3744
+ socket.on("error", (error) => {
3745
+ clearTimeout(timer);
3746
+ reject(error);
3747
+ });
3748
+ socket.write(`${payload}
3749
+ `);
3750
+ });
3751
+ }
3752
+ async function ensureJudgeBrokerDaemon(stateDir, repoRoot, sessionConfig) {
3753
+ const paths = judgeBrokerPaths(stateDir);
3754
+ const brokerSessionConfig = normalizeJudgeSessionConfig({ ...sessionConfig, enabled: true });
3755
+ const nextConfigPayload = brokerSessionConfigPayload(brokerSessionConfig);
3756
+ const existingConfig = await readBrokerSessionConfig(paths.sessionConfigPath);
3757
+ const configChanged = existingConfig !== null && brokerSessionConfigPayload(existingConfig) !== nextConfigPayload;
3758
+ const status = await readBrokerStatus(paths.statusPath);
3759
+ if (status && isProcessAlive(status.pid) && existsSync5(paths.socketPath) && !configChanged) {
3760
+ await writeBrokerSessionConfig(paths, brokerSessionConfig);
3761
+ return paths;
3762
+ }
3763
+ if (status?.pid && isProcessAlive(status.pid)) {
3764
+ await stopJudgeBrokerDaemon(stateDir);
3765
+ } else {
3766
+ await cleanupJudgeBrokerArtifacts(paths);
3767
+ }
3768
+ await writeBrokerSessionConfig(paths, brokerSessionConfig);
3769
+ const child = spawn2(process.execPath, [daemonScriptPath()], {
3770
+ detached: true,
3771
+ stdio: "ignore",
3772
+ env: {
3773
+ ...process.env,
3774
+ BELAY_JUDGE_BROKER_REPO_ROOT: repoRoot,
3775
+ BELAY_JUDGE_BROKER_STATE_DIR: stateDir
3776
+ }
3777
+ });
3778
+ child.unref();
3779
+ const deadline = Date.now() + sessionConfig.connectTimeoutMs;
3780
+ while (Date.now() < deadline) {
3781
+ if (existsSync5(paths.socketPath)) {
3782
+ const live = await readBrokerStatus(paths.statusPath);
3783
+ if (live && isProcessAlive(live.pid)) {
3784
+ return paths;
3785
+ }
3786
+ }
3787
+ await new Promise((resolve) => setTimeout(resolve, 25));
3788
+ }
3789
+ throw new Error("judge broker daemon failed to start");
3790
+ }
3791
+ async function cleanupJudgeBrokerArtifacts(paths) {
3792
+ for (const artifact of [
3793
+ paths.socketPath,
3794
+ paths.statusPath,
3795
+ paths.pidPath,
3796
+ paths.sessionConfigPath
3797
+ ]) {
3798
+ if (existsSync5(artifact)) {
3799
+ await unlink2(artifact).catch(() => void 0);
3800
+ }
3801
+ }
3802
+ }
3803
+ async function stopJudgeBrokerDaemon(stateDir) {
3804
+ const paths = judgeBrokerPaths(stateDir);
3805
+ const status = await readBrokerStatus(paths.statusPath);
3806
+ if (status?.pid && isProcessAlive(status.pid)) {
3807
+ try {
3808
+ process.kill(status.pid, "SIGTERM");
3809
+ } catch {
3810
+ }
3811
+ }
3812
+ await cleanupJudgeBrokerArtifacts(paths);
3813
+ return status?.pid ? 1 : 0;
3814
+ }
3815
+ async function evaluateViaJudgeBroker(options) {
3816
+ const runCommand = options.runCommand ?? ((invocation, timeout) => runCliJsonWithTimeouts(invocation, { evalTimeoutMs: timeout }));
3817
+ if (useInProcessBroker()) {
3818
+ const broker = getRepoJudgeSessionBroker(options.repoRoot, {
3819
+ config: options.sessionConfig,
3820
+ runCommand
3821
+ });
3822
+ return broker.evaluate(options.request, options.timeoutMs);
3823
+ }
3824
+ const paths = await ensureJudgeBrokerDaemon(
3825
+ options.stateDir,
3826
+ options.repoRoot,
3827
+ options.sessionConfig
3828
+ );
3829
+ return rpcEvaluate(
3830
+ paths.socketPath,
3831
+ options.request,
3832
+ options.timeoutMs,
3833
+ options.sessionConfig.connectTimeoutMs
3834
+ );
3835
+ }
3836
+ async function rpcInvalidate(socketPath, sessionKey, reason, connectTimeoutMs) {
3837
+ const socket = await connectBrokerSocket(socketPath, connectTimeoutMs);
3838
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
3839
+ const payload = JSON.stringify({ type: "invalidate", id, sessionKey, reason });
3840
+ await new Promise((resolve, reject) => {
3841
+ let buffer = "";
3842
+ const timer = setTimeout(() => {
3843
+ socket.destroy();
3844
+ reject(new Error("judge broker invalidate timed out"));
3845
+ }, connectTimeoutMs);
3846
+ const onData = (chunk) => {
3847
+ buffer += String(chunk);
3848
+ const lines = buffer.split("\n");
3849
+ buffer = lines.pop() ?? "";
3850
+ for (const line of lines) {
3851
+ if (!line.trim()) {
3852
+ continue;
3853
+ }
3854
+ try {
3855
+ const message = JSON.parse(line);
3856
+ if (message.id !== id) {
3857
+ continue;
3858
+ }
3859
+ clearTimeout(timer);
3860
+ socket.destroy();
3861
+ resolve();
3862
+ } catch {
3863
+ }
3864
+ }
3865
+ };
3866
+ socket.on("data", onData);
3867
+ socket.on("error", (error) => {
3868
+ clearTimeout(timer);
3869
+ reject(error);
3870
+ });
3871
+ socket.write(`${payload}
3872
+ `);
3873
+ });
3874
+ }
3875
+ async function invalidateJudgeSession(options) {
3876
+ if (useInProcessBroker()) {
3877
+ const broker = getRepoJudgeSessionBroker(options.repoRoot, {
3878
+ config: { ...options.sessionConfig, enabled: true },
3879
+ runCommand: (invocation, timeout) => runCliJsonWithTimeouts(invocation, { evalTimeoutMs: timeout })
3880
+ });
3881
+ broker.invalidateSession(options.sessionKey, options.reason);
3882
+ return;
3883
+ }
3884
+ const paths = await ensureJudgeBrokerDaemon(
3885
+ options.stateDir,
3886
+ options.repoRoot,
3887
+ options.sessionConfig
3888
+ );
3889
+ await rpcInvalidate(
3890
+ paths.socketPath,
3891
+ options.sessionKey,
3892
+ options.reason,
3893
+ options.sessionConfig.connectTimeoutMs
3894
+ );
3895
+ }
3896
+
3897
+ // src/core/verdict/judge-cli-fingerprint.ts
3898
+ import { spawn as spawn3 } from "node:child_process";
3899
+ var CLI_BINARIES = {
3900
+ codex: "codex",
3901
+ cursor: "cursor-agent",
3902
+ claude: "claude"
3903
+ };
3904
+ var fingerprintResolver = null;
3905
+ var fingerprintCache = /* @__PURE__ */ new Map();
3906
+ async function runVersionProbe(binary, timeoutMs) {
3907
+ return new Promise((resolve, reject) => {
3908
+ const child = spawn3(binary, ["--version"], { stdio: ["ignore", "pipe", "pipe"] });
3909
+ let stdout = "";
3910
+ let stderr = "";
3911
+ const timer = setTimeout(() => {
3912
+ child.kill("SIGTERM");
3913
+ reject(new Error(`${binary} --version timed out`));
3914
+ }, timeoutMs);
3915
+ child.stdout.on("data", (chunk) => {
3916
+ stdout += String(chunk);
3917
+ });
3918
+ child.stderr.on("data", (chunk) => {
3919
+ stderr += String(chunk);
3920
+ });
3921
+ child.on("error", (error) => {
3922
+ clearTimeout(timer);
3923
+ reject(error);
3924
+ });
3925
+ child.on("close", (code) => {
3926
+ clearTimeout(timer);
3927
+ const output = (stdout || stderr).trim();
3928
+ if (code === 0 && output) {
3929
+ resolve(output.split(/\r?\n/)[0]?.trim() || output);
3930
+ return;
3931
+ }
3932
+ reject(
3933
+ new Error(stderr.trim() || `${binary} --version exited with code ${code ?? "unknown"}`)
3934
+ );
3935
+ });
3936
+ });
3937
+ }
3938
+ async function resolveCliVersionFingerprint(providerId, timeoutMs = 3e3) {
3939
+ const cacheKey = providerId;
3940
+ const cached = fingerprintCache.get(cacheKey);
3941
+ if (cached) {
3942
+ return cached;
3943
+ }
3944
+ if (fingerprintResolver) {
3945
+ const resolved = await fingerprintResolver(providerId);
3946
+ fingerprintCache.set(cacheKey, resolved);
3947
+ return resolved;
3948
+ }
3949
+ const binary = CLI_BINARIES[providerId];
3950
+ const version = await runVersionProbe(binary, timeoutMs);
3951
+ fingerprintCache.set(cacheKey, version);
3952
+ return version;
3953
+ }
3954
+
3955
+ // src/core/verdict/judge-provider-matrix.ts
3956
+ var JUDGE_PROVIDER_SESSION_MATRIX = {
3957
+ cursor: {
3958
+ providerId: "cursor",
3959
+ supportsSession: true,
3960
+ resumeFlag: "--resume",
3961
+ createChatFlag: null,
3962
+ readOnlyArgs: ["--mode", "ask", "--sandbox", "enabled", "--trust"],
3963
+ rejectedUnsafeOptions: ["--force", "--yolo", "--dangerously-skip-permissions"],
3964
+ failureModes: [
3965
+ "parse_error",
3966
+ "non_json_response",
3967
+ "timeout",
3968
+ "cli_nonzero_exit",
3969
+ "io_error",
3970
+ "version_mismatch"
3971
+ ],
3972
+ notes: "Pilot provider: ask mode + sandbox enabled; resume via --resume <chatId>."
3973
+ },
3974
+ codex: {
3975
+ providerId: "codex",
3976
+ supportsSession: false,
3977
+ resumeFlag: null,
3978
+ createChatFlag: null,
3979
+ readOnlyArgs: ["--sandbox", "read-only"],
3980
+ rejectedUnsafeOptions: ["--dangerously-bypass-approvals-and-sandbox"],
3981
+ failureModes: ["unsupported_resume", "parse_error", "timeout"],
3982
+ notes: "Session transport disabled by default; spawn read-only exec remains canonical."
3983
+ },
3984
+ claude: {
3985
+ providerId: "claude",
3986
+ supportsSession: false,
3987
+ resumeFlag: "--continue",
3988
+ createChatFlag: null,
3989
+ readOnlyArgs: ["--permission-mode", "plan", "--tools", "", "--bare"],
3990
+ rejectedUnsafeOptions: ["--dangerously-skip-permissions"],
3991
+ failureModes: ["unsupported_resume", "parse_error", "timeout"],
3992
+ notes: "Session transport gated behind allowlist after capability verification."
3993
+ }
3994
+ };
3995
+ function providerSupportsSession(providerId, allowlist) {
3996
+ const capability = JUDGE_PROVIDER_SESSION_MATRIX[providerId];
3997
+ return capability.supportsSession && allowlist.includes(providerId);
3998
+ }
3999
+ function assertReadOnlyInvocationArgs(providerId, args) {
4000
+ const capability = JUDGE_PROVIDER_SESSION_MATRIX[providerId];
4001
+ for (const unsafe of capability.rejectedUnsafeOptions) {
4002
+ if (args.includes(unsafe)) {
4003
+ return { ok: false, reason: "unsafe_option_rejected" };
4004
+ }
4005
+ }
4006
+ for (let index = 0; index < capability.readOnlyArgs.length; index += 2) {
4007
+ const flag = capability.readOnlyArgs[index];
4008
+ const value = capability.readOnlyArgs[index + 1];
4009
+ if (!flag) {
4010
+ continue;
4011
+ }
4012
+ const flagIndex = args.indexOf(flag);
4013
+ if (flagIndex === -1) {
4014
+ return { ok: false, reason: "unsafe_option_rejected" };
4015
+ }
4016
+ if (value !== void 0 && args[flagIndex + 1] !== value) {
4017
+ return { ok: false, reason: "unsafe_option_rejected" };
4018
+ }
4019
+ }
4020
+ return { ok: true };
4021
+ }
4022
+
4023
+ // src/core/verdict/judge-session-kill-switch.ts
4024
+ import { existsSync as existsSync6 } from "node:fs";
4025
+ import { mkdir as mkdir3, readFile as readFile3, unlink as unlink3, writeFile as writeFile3 } from "node:fs/promises";
4026
+ import path18 from "node:path";
4027
+ var JUDGE_SESSION_KILL_FILE = "judge-session-kill.json";
4028
+ function judgeSessionKillSwitchPath(stateDir) {
4029
+ return path18.join(stateDir, JUDGE_SESSION_KILL_FILE);
4030
+ }
4031
+ async function readJudgeSessionKillSwitch(stateDir) {
4032
+ const filePath = judgeSessionKillSwitchPath(stateDir);
4033
+ if (!existsSync6(filePath)) {
4034
+ return null;
4035
+ }
4036
+ try {
4037
+ const raw = JSON.parse(await readFile3(filePath, "utf8"));
4038
+ return raw.triggered === true ? raw : null;
4039
+ } catch {
4040
+ return null;
4041
+ }
4042
+ }
4043
+ async function isJudgeSessionKillSwitchPersisted(stateDir) {
4044
+ const record = await readJudgeSessionKillSwitch(stateDir);
4045
+ return record?.triggered === true;
4046
+ }
4047
+ async function persistJudgeSessionKillSwitch(stateDir, reason = "shadow_mismatch") {
4048
+ await mkdir3(stateDir, { recursive: true, mode: 448 });
4049
+ const record = {
4050
+ triggered: true,
4051
+ at: (/* @__PURE__ */ new Date()).toISOString(),
4052
+ reason
4053
+ };
4054
+ await writeFile3(judgeSessionKillSwitchPath(stateDir), JSON.stringify(record), {
4055
+ encoding: "utf8",
4056
+ mode: 384
4057
+ });
4058
+ }
4059
+
4060
+ // src/core/verdict/judge-shadow.ts
4061
+ var shadowStates = /* @__PURE__ */ new Map();
4062
+ var clock = () => /* @__PURE__ */ new Date();
4063
+ var random = Math.random;
4064
+ function dailyKeyFor(date) {
4065
+ return date.toISOString().slice(0, 10);
4066
+ }
4067
+ function getShadowState(repoRoot) {
4068
+ const existing = shadowStates.get(repoRoot);
4069
+ if (existing) {
4070
+ const key = dailyKeyFor(clock());
4071
+ if (existing.dailyKey !== key) {
4072
+ existing.dailyCount = 0;
4073
+ existing.dailyKey = key;
4074
+ }
4075
+ return existing;
4076
+ }
4077
+ const created = {
4078
+ compared: 0,
4079
+ mismatches: 0,
4080
+ dailyCount: 0,
4081
+ dailyKey: dailyKeyFor(clock()),
4082
+ killSwitchTriggered: false,
4083
+ mismatchWindow: []
4084
+ };
4085
+ shadowStates.set(repoRoot, created);
4086
+ return created;
4087
+ }
4088
+ async function isJudgeSessionKillSwitchActive(repoRoot, stateDir) {
4089
+ if (getShadowState(repoRoot).killSwitchTriggered) {
4090
+ return true;
4091
+ }
4092
+ if (stateDir) {
4093
+ return isJudgeSessionKillSwitchPersisted(stateDir);
4094
+ }
4095
+ return false;
4096
+ }
4097
+ async function triggerJudgeSessionKillSwitch(repoRoot, stateDir, reason = "shadow_mismatch") {
4098
+ getShadowState(repoRoot).killSwitchTriggered = true;
4099
+ if (stateDir) {
4100
+ await persistJudgeSessionKillSwitch(stateDir, reason);
4101
+ }
4102
+ }
4103
+ function shouldRunShadowComparison(repoRoot, providerId, config) {
4104
+ if (!config.enabled) {
4105
+ return false;
4106
+ }
4107
+ if (!config.providerAllowlist.includes(providerId)) {
4108
+ return false;
4109
+ }
4110
+ const state = getShadowState(repoRoot);
4111
+ if (state.killSwitchTriggered) {
4112
+ return false;
4113
+ }
4114
+ if (state.dailyCount >= config.dailyRequestCap) {
4115
+ return false;
4116
+ }
4117
+ const rate = Math.min(config.sampleRate, config.sampleRateMax);
4118
+ return random() < rate;
4119
+ }
4120
+ function verdictsEquivalent(a, b) {
4121
+ return a.local_recoverable === b.local_recoverable && a.destroys_history_or_secrets === b.destroys_history_or_secrets;
4122
+ }
4123
+ function recordShadowComparison(repoRoot, config, mismatch) {
4124
+ const state = getShadowState(repoRoot);
4125
+ state.compared += 1;
4126
+ state.dailyCount += 1;
4127
+ if (mismatch) {
4128
+ state.mismatches += 1;
4129
+ }
4130
+ state.mismatchWindow.push(mismatch);
4131
+ if (state.mismatchWindow.length > config.windowSize) {
4132
+ state.mismatchWindow.shift();
4133
+ }
4134
+ const windowComparisons = state.mismatchWindow.length;
4135
+ const windowMismatches = state.mismatchWindow.filter(Boolean).length;
4136
+ const mismatchRateWindow = windowComparisons === 0 ? 0 : windowMismatches / windowComparisons;
4137
+ if (windowComparisons >= Math.min(10, config.windowSize) && mismatchRateWindow > config.mismatchRateThreshold) {
4138
+ state.killSwitchTriggered = true;
4139
+ }
4140
+ return {
4141
+ compared: true,
4142
+ mismatch,
4143
+ mismatchRateWindow,
4144
+ killSwitchTriggered: state.killSwitchTriggered
4145
+ };
4146
+ }
4147
+
4148
+ // src/core/verdict/judge-transport.ts
4149
+ function isEvalTimeoutError(error) {
4150
+ const message = error instanceof Error ? error.message : String(error);
4151
+ return /timed out/i.test(message);
4152
+ }
4153
+ function classifyParseFailure(providerId, raw) {
4154
+ const trimmed = raw.trim();
4155
+ if (!trimmed) {
4156
+ return "non_json_response";
4157
+ }
4158
+ if (parseCliJudgeOutput(providerId, trimmed)) {
4159
+ return "parse_error";
4160
+ }
4161
+ try {
4162
+ JSON.parse(trimmed);
4163
+ return "parse_error";
4164
+ } catch {
4165
+ return "non_json_response";
4166
+ }
4167
+ }
4168
+ function spawnFallbackReason(providerId, error) {
4169
+ if (error instanceof CliRunError) {
4170
+ if (error.kind === "timeout") {
4171
+ return "eval_timeout";
4172
+ }
4173
+ if (error.kind === "exit_nonzero") {
4174
+ return `${providerId}_cli_nonzero`;
4175
+ }
4176
+ if (error.kind === "spawn_error") {
4177
+ return `${providerId}_cli_spawn_error`;
4178
+ }
4179
+ }
4180
+ if (isEvalTimeoutError(error)) {
4181
+ return "eval_timeout";
4182
+ }
4183
+ return `${providerId}_cli_unavailable`;
4184
+ }
4185
+ function parseWithBudget(providerId, raw) {
4186
+ const started = Date.now();
4187
+ const verdict2 = parseCliJudgeOutput(providerId, raw);
4188
+ const parseMs = Date.now() - started;
4189
+ recordJudgeLatency("tier1_parse", parseMs);
4190
+ return { verdict: verdict2, parseMs };
4191
+ }
4192
+ async function runSpawnTransport(request, runCommand) {
4193
+ const { context, prompt } = request;
4194
+ const sessionConfig = context.runtime.session;
4195
+ const invocation = buildCliInvocation(context.providerId, prompt, context.model);
4196
+ const readOnly = assertReadOnlyInvocationArgs(context.providerId, invocation.args);
4197
+ if (!readOnly.ok) {
4198
+ return {
4199
+ raw: "",
4200
+ verdict: null,
4201
+ transport: "spawn",
4202
+ sessionUsed: false,
4203
+ sessionReused: false,
4204
+ fallbackReason: "unsafe_option_rejected",
4205
+ connectMs: 0,
4206
+ evalMs: 0,
4207
+ parseMs: 0
4208
+ };
4209
+ }
4210
+ const evalTimeoutMs = resolveSessionEvalTimeoutMs(sessionConfig, context.judgeTimeoutMs);
4211
+ let raw = "";
4212
+ let evalMs = 0;
4213
+ const evalStarted = Date.now();
4214
+ try {
4215
+ raw = await runCommand(invocation, evalTimeoutMs);
4216
+ evalMs = Date.now() - evalStarted;
4217
+ recordJudgeLatency("tier1_spawn", evalMs);
4218
+ } catch (error) {
4219
+ evalMs = Date.now() - evalStarted;
4220
+ return {
4221
+ raw: "",
4222
+ verdict: null,
4223
+ transport: "spawn",
4224
+ sessionUsed: false,
4225
+ sessionReused: false,
4226
+ fallbackReason: spawnFallbackReason(context.providerId, error),
4227
+ connectMs: 0,
4228
+ evalMs,
4229
+ parseMs: 0
4230
+ };
4231
+ }
4232
+ const parsed = parseWithBudget(context.providerId, raw);
4233
+ return {
4234
+ raw,
4235
+ verdict: parsed.verdict,
4236
+ transport: "spawn",
4237
+ sessionUsed: false,
4238
+ sessionReused: false,
4239
+ fallbackReason: parsed.verdict ? void 0 : classifyParseFailure(context.providerId, raw),
4240
+ connectMs: 0,
4241
+ evalMs,
4242
+ parseMs: parsed.parseMs
4243
+ };
4244
+ }
4245
+ async function runSessionTransport(request, runCommand) {
4246
+ const { context, prompt } = request;
4247
+ const sessionConfig = context.runtime.session;
4248
+ const connectStarted = Date.now();
4249
+ if (!sessionConfig.enabled || !providerSupportsSession(context.providerId, sessionConfig.providerAllowlist) || await isJudgeSessionKillSwitchActive(context.repoRoot, context.stateDir)) {
4250
+ return runSpawnTransport(request, runCommand);
4251
+ }
4252
+ let cliVersion = "unknown";
4253
+ try {
4254
+ cliVersion = await resolveCliVersionFingerprint(
4255
+ context.providerId,
4256
+ sessionConfig.connectTimeoutMs
4257
+ );
4258
+ } catch {
4259
+ return {
4260
+ ...await runSpawnTransport(request, runCommand),
4261
+ fallbackReason: "connect_timeout",
4262
+ sessionUsed: false,
4263
+ sessionReused: false
4264
+ };
4265
+ }
4266
+ const connectMs = Date.now() - connectStarted;
4267
+ recordJudgeLatency("tier1_connect", connectMs);
4268
+ const promptBytes = Buffer.byteLength(prompt, "utf8");
4269
+ if (exceedsPromptBudget(promptBytes, sessionConfig)) {
4270
+ const spawnResult = await runSpawnTransport(request, runCommand);
4271
+ return {
4272
+ ...spawnResult,
4273
+ sessionUsed: false,
4274
+ sessionReused: false,
4275
+ sessionResetReason: "max_prompt_bytes_exceeded",
4276
+ fallbackReason: "guard_reset"
4277
+ };
4278
+ }
4279
+ const keyParts = {
4280
+ providerId: context.providerId,
4281
+ model: context.model,
4282
+ repoRoot: context.repoRoot,
4283
+ judgeMode: context.judgeMode,
4284
+ cliVersion
4285
+ };
4286
+ const sessionKey = buildJudgeSessionKey(keyParts);
4287
+ const sessionRefHash = hashJudgeSessionRef(sessionKey);
4288
+ const invocation = buildCliInvocation(context.providerId, prompt, context.model);
4289
+ const readOnly = assertReadOnlyInvocationArgs(context.providerId, invocation.args);
4290
+ if (!readOnly.ok) {
4291
+ return runSpawnTransport(request, runCommand);
4292
+ }
4293
+ const evalTimeoutMs = resolveSessionEvalTimeoutMs(sessionConfig, context.judgeTimeoutMs);
4294
+ const evalStarted = Date.now();
4295
+ let brokerResult;
4296
+ try {
4297
+ brokerResult = await evaluateViaJudgeBroker({
4298
+ stateDir: context.stateDir,
4299
+ repoRoot: context.repoRoot,
4300
+ sessionConfig,
4301
+ request: {
4302
+ keyParts,
4303
+ invocation,
4304
+ promptBytes
4305
+ },
4306
+ timeoutMs: evalTimeoutMs,
4307
+ runCommand
4308
+ });
4309
+ } catch (error) {
4310
+ const spawnResult = await runSpawnTransport(request, runCommand);
4311
+ return {
4312
+ ...spawnResult,
4313
+ transport: "session",
4314
+ sessionUsed: true,
4315
+ sessionReused: false,
4316
+ fallbackReason: spawnFallbackReason(context.providerId, error),
4317
+ sessionResetReason: "cli_error",
4318
+ sessionRefHash
4319
+ };
4320
+ }
4321
+ const evalMs = Date.now() - evalStarted;
4322
+ recordJudgeLatency("tier1_session", evalMs);
4323
+ recordJudgeLatency("tier1_eval", evalMs);
4324
+ const parsed = parseWithBudget(context.providerId, brokerResult.raw);
4325
+ if (!parsed.verdict) {
4326
+ const parseReason = classifyParseFailure(context.providerId, brokerResult.raw);
4327
+ await invalidateJudgeSession({
4328
+ stateDir: context.stateDir,
4329
+ repoRoot: context.repoRoot,
4330
+ sessionConfig,
4331
+ sessionKey,
4332
+ reason: parseReason === "non_json_response" ? "non_json_response" : "parse_failure"
4333
+ });
4334
+ const spawnResult = await runSpawnTransport(request, runCommand);
4335
+ return {
4336
+ ...spawnResult,
4337
+ transport: "session",
4338
+ sessionUsed: true,
4339
+ sessionReused: brokerResult.reused,
4340
+ fallbackReason: guardFailClosedFallbackReason(
4341
+ parseReason === "non_json_response" ? "non_json_response" : "parse_failure"
4342
+ ),
4343
+ sessionResetReason: parseReason === "non_json_response" ? "non_json_response" : "parse_failure",
4344
+ sessionRefHash
4345
+ };
4346
+ }
4347
+ return {
4348
+ raw: brokerResult.raw,
4349
+ verdict: parsed.verdict,
4350
+ transport: "session",
4351
+ sessionUsed: true,
4352
+ sessionReused: brokerResult.reused,
4353
+ sessionResetReason: brokerResult.resetReason,
4354
+ sessionRefHash,
4355
+ connectMs,
4356
+ evalMs,
4357
+ parseMs: parsed.parseMs
4358
+ };
4359
+ }
4360
+ async function evaluateWithJudgeTransport(request, options = {}) {
4361
+ const runCommand = options.runCommand ?? ((invocation, timeoutMs) => runCliJsonWithTimeouts(invocation, { evalTimeoutMs: timeoutMs }));
4362
+ const { context } = request;
4363
+ const useSession = context.runtime.session.enabled && providerSupportsSession(context.providerId, context.runtime.session.providerAllowlist) && !await isJudgeSessionKillSwitchActive(context.repoRoot, context.stateDir);
4364
+ const primary = useSession ? await runSessionTransport(request, runCommand) : await runSpawnTransport(request, runCommand);
4365
+ if (!shouldRunShadowComparison(context.repoRoot, context.providerId, context.runtime.shadow)) {
4366
+ return primary;
4367
+ }
4368
+ const shadowSpawn = primary.transport === "session" ? await runSpawnTransport(request, runCommand) : primary;
4369
+ if (!primary.verdict || !shadowSpawn.verdict) {
4370
+ return primary;
4371
+ }
4372
+ const shadow = recordShadowComparison(
4373
+ context.repoRoot,
4374
+ context.runtime.shadow,
4375
+ !verdictsEquivalent(primary.verdict, shadowSpawn.verdict)
4376
+ );
4377
+ if (shadow.killSwitchTriggered) {
4378
+ await triggerJudgeSessionKillSwitch(context.repoRoot, context.stateDir);
4379
+ }
4380
+ return {
4381
+ ...primary,
4382
+ shadowCompared: shadow.compared,
4383
+ shadowMismatch: shadow.mismatch,
4384
+ shadowMismatchRateWindow: shadow.mismatchRateWindow,
4385
+ killSwitchTriggered: shadow.killSwitchTriggered,
4386
+ verdict: shadow.mismatch ? shadowSpawn.verdict : primary.verdict,
4387
+ fallbackReason: shadow.mismatch ? "shadow_forced_spawn" : primary.fallbackReason,
4388
+ transport: shadow.mismatch ? "spawn" : primary.transport,
4389
+ sessionUsed: shadow.mismatch ? false : primary.sessionUsed,
4390
+ sessionReused: shadow.mismatch ? false : primary.sessionReused
4391
+ };
2612
4392
  }
2613
4393
 
2614
4394
  // src/core/verdict/judge-cli.ts
2615
- import { spawn } from "node:child_process";
2616
- var CLI_BINARIES = {
4395
+ var CliRunError = class extends Error {
4396
+ kind;
4397
+ exitCode;
4398
+ stderr;
4399
+ stdout;
4400
+ command;
4401
+ constructor(kind, message, details = {}) {
4402
+ super(message);
4403
+ this.name = "CliRunError";
4404
+ this.kind = kind;
4405
+ this.exitCode = details.exitCode;
4406
+ this.stderr = details.stderr;
4407
+ this.stdout = details.stdout;
4408
+ this.command = details.command;
4409
+ }
4410
+ };
4411
+ var CLI_BINARIES2 = {
2617
4412
  codex: "codex",
2618
4413
  cursor: "cursor-agent",
2619
4414
  claude: "claude"
2620
4415
  };
4416
+ var FORCE_KILL_GRACE_MS = 250;
2621
4417
  function failClosedVerdict2(reason) {
2622
4418
  return {
2623
4419
  local_recoverable: false,
@@ -2648,7 +4444,7 @@ function initialTrace(options) {
2648
4444
  function buildCliInvocation(providerId, prompt, model) {
2649
4445
  if (providerId === "codex") {
2650
4446
  return {
2651
- binary: CLI_BINARIES[providerId],
4447
+ binary: CLI_BINARIES2[providerId],
2652
4448
  args: [
2653
4449
  "exec",
2654
4450
  "--json",
@@ -2664,7 +4460,7 @@ function buildCliInvocation(providerId, prompt, model) {
2664
4460
  }
2665
4461
  if (providerId === "cursor") {
2666
4462
  return {
2667
- binary: CLI_BINARIES[providerId],
4463
+ binary: CLI_BINARIES2[providerId],
2668
4464
  args: [
2669
4465
  "--print",
2670
4466
  "--output-format",
@@ -2681,7 +4477,7 @@ function buildCliInvocation(providerId, prompt, model) {
2681
4477
  };
2682
4478
  }
2683
4479
  return {
2684
- binary: CLI_BINARIES[providerId],
4480
+ binary: CLI_BINARIES2[providerId],
2685
4481
  args: [
2686
4482
  "-p",
2687
4483
  "--output-format",
@@ -2770,17 +4566,64 @@ function parseCliJudgeOutput(providerId, raw) {
2770
4566
  return parseCliJsonLines(raw);
2771
4567
  }
2772
4568
  async function runCliJson(invocation, timeoutMs) {
4569
+ return runCliJsonWithTimeouts(invocation, { evalTimeoutMs: timeoutMs });
4570
+ }
4571
+ async function runCliJsonWithTimeouts(invocation, budgets) {
2773
4572
  const { binary, args, stdin } = invocation;
4573
+ const connectTimeoutMs = budgets.connectTimeoutMs ?? 0;
4574
+ if (connectTimeoutMs > 0) {
4575
+ await new Promise((resolve) => setTimeout(resolve, 0));
4576
+ }
2774
4577
  return new Promise((resolve, reject) => {
2775
- const child = spawn(binary, args, {
4578
+ const child = spawn4(binary, args, {
2776
4579
  stdio: ["pipe", "pipe", "pipe"]
2777
4580
  });
4581
+ const command = [binary, ...args].join(" ");
2778
4582
  let stdout = "";
2779
4583
  let stderr = "";
2780
- const timer = setTimeout(() => {
2781
- child.kill("SIGTERM");
2782
- reject(new Error(`${binary} timed out`));
2783
- }, timeoutMs);
4584
+ let settled = false;
4585
+ let timer = null;
4586
+ let forceKillTimer = null;
4587
+ const cleanup = () => {
4588
+ if (timer) {
4589
+ clearTimeout(timer);
4590
+ }
4591
+ if (forceKillTimer) {
4592
+ clearTimeout(forceKillTimer);
4593
+ }
4594
+ };
4595
+ const rejectOnce = (error) => {
4596
+ if (settled) return;
4597
+ settled = true;
4598
+ cleanup();
4599
+ reject(error);
4600
+ };
4601
+ const resolveOnce = (value) => {
4602
+ if (settled) return;
4603
+ settled = true;
4604
+ cleanup();
4605
+ resolve(value);
4606
+ };
4607
+ timer = setTimeout(() => {
4608
+ rejectOnce(
4609
+ new CliRunError("timeout", `${binary} timed out`, {
4610
+ stderr: stderr.trim() || void 0,
4611
+ stdout: stdout.trim() || void 0,
4612
+ command
4613
+ })
4614
+ );
4615
+ try {
4616
+ child.kill("SIGTERM");
4617
+ } catch {
4618
+ return;
4619
+ }
4620
+ forceKillTimer = setTimeout(() => {
4621
+ try {
4622
+ child.kill("SIGKILL");
4623
+ } catch {
4624
+ }
4625
+ }, FORCE_KILL_GRACE_MS);
4626
+ }, budgets.evalTimeoutMs);
2784
4627
  child.stdout.on("data", (chunk) => {
2785
4628
  stdout += String(chunk);
2786
4629
  });
@@ -2792,16 +4635,30 @@ async function runCliJson(invocation, timeoutMs) {
2792
4635
  }
2793
4636
  child.stdin.end();
2794
4637
  child.on("error", (error) => {
2795
- clearTimeout(timer);
2796
- reject(error);
4638
+ rejectOnce(
4639
+ new CliRunError("spawn_error", error instanceof Error ? error.message : String(error), {
4640
+ command
4641
+ })
4642
+ );
2797
4643
  });
2798
4644
  child.on("close", (code) => {
2799
- clearTimeout(timer);
4645
+ if (settled) return;
2800
4646
  if (code === 0 && stdout.trim()) {
2801
- resolve(stdout.trim());
4647
+ resolveOnce(stdout.trim());
2802
4648
  return;
2803
4649
  }
2804
- reject(new Error(stderr.trim() || `${binary} exited with code ${code ?? "unknown"}`));
4650
+ rejectOnce(
4651
+ new CliRunError(
4652
+ "exit_nonzero",
4653
+ stderr.trim() || `${binary} exited with code ${code ?? "unknown"}`,
4654
+ {
4655
+ exitCode: typeof code === "number" ? code : void 0,
4656
+ stderr: stderr.trim() || void 0,
4657
+ stdout: stdout.trim() || void 0,
4658
+ command
4659
+ }
4660
+ )
4661
+ );
2805
4662
  });
2806
4663
  });
2807
4664
  }
@@ -2810,6 +4667,10 @@ function createCliJudge(options) {
2810
4667
  const timeoutMs = options.timeoutMs ?? 25e3;
2811
4668
  const providerId = options.providerId;
2812
4669
  const runCommand = options.runCliCommand ?? runCliJson;
4670
+ const runtime = options.runtime ?? DEFAULT_JUDGE_RUNTIME_CONFIG;
4671
+ const repoRoot = options.repoRoot ?? process.cwd();
4672
+ const stateDir = options.stateDir ?? repoRoot;
4673
+ const judgeMode = options.judgeMode ?? "enforce";
2813
4674
  const judge = {
2814
4675
  lastTrace: initialTrace(options),
2815
4676
  async evaluate(input) {
@@ -2841,37 +4702,74 @@ function createCliJudge(options) {
2841
4702
  }
2842
4703
  const prompt = buildTier1Prompt(scrubbed.text);
2843
4704
  try {
2844
- const raw = await runCommand(
2845
- buildCliInvocation(providerId, prompt, options.modelResolved),
2846
- timeoutMs
4705
+ const transportResult = await evaluateWithJudgeTransport(
4706
+ {
4707
+ prompt,
4708
+ context: {
4709
+ providerId,
4710
+ model: options.modelResolved,
4711
+ repoRoot: input.context.repoRoot || repoRoot,
4712
+ stateDir,
4713
+ judgeMode,
4714
+ runtime,
4715
+ judgeTimeoutMs: timeoutMs
4716
+ }
4717
+ },
4718
+ { runCommand }
2847
4719
  );
2848
- const parsed = parseCliJudgeOutput(providerId, raw);
2849
- if (!parsed) {
4720
+ const traceTransport = transportResult.transport === "session" ? `${providerId}-cli-session` : transport;
4721
+ if (!transportResult.verdict) {
4722
+ const fallbackReason = transportResult.fallbackReason ?? `${providerId}_cli_parse_error`;
2850
4723
  judge.lastTrace = {
2851
4724
  provider: "fallback",
2852
4725
  modelRequested: options.modelRequested,
2853
4726
  modelResolved: options.modelResolved,
2854
4727
  latencyMs: Date.now() - started,
2855
- fallbackReason: `${providerId}_cli_parse_error`
4728
+ fallbackReason,
4729
+ judgeFallbackReason: fallbackReason,
4730
+ judgeSessionUsed: transportResult.sessionUsed,
4731
+ judgeSessionReused: transportResult.sessionReused,
4732
+ judgeSessionRefHash: transportResult.sessionRefHash,
4733
+ judgeSessionResetReason: transportResult.sessionResetReason,
4734
+ judgeConnectMs: transportResult.connectMs,
4735
+ judgeEvalMs: transportResult.evalMs,
4736
+ judgeParseMs: transportResult.parseMs,
4737
+ judgeShadowCompared: transportResult.shadowCompared,
4738
+ judgeShadowMismatch: transportResult.shadowMismatch,
4739
+ judgeShadowMismatchRateWindow: transportResult.shadowMismatchRateWindow,
4740
+ judgeKillSwitchTriggered: transportResult.killSwitchTriggered
2856
4741
  };
2857
- return failClosedVerdict2(`${providerId}_cli_parse_error`);
4742
+ return failClosedVerdict2(transportFallbackToFailClosedReason(providerId, fallbackReason));
2858
4743
  }
2859
4744
  judge.lastTrace = {
2860
4745
  provider: traceProvider(options.providerId),
2861
4746
  modelRequested: options.modelRequested,
2862
4747
  modelResolved: options.modelResolved,
2863
4748
  latencyMs: Date.now() - started,
2864
- transport,
2865
- outboundRedacted: true
4749
+ transport: traceTransport,
4750
+ outboundRedacted: true,
4751
+ judgeSessionUsed: transportResult.sessionUsed,
4752
+ judgeSessionReused: transportResult.sessionReused,
4753
+ judgeSessionRefHash: transportResult.sessionRefHash,
4754
+ judgeSessionResetReason: transportResult.sessionResetReason,
4755
+ judgeFallbackReason: transportResult.fallbackReason,
4756
+ judgeConnectMs: transportResult.connectMs,
4757
+ judgeEvalMs: transportResult.evalMs,
4758
+ judgeParseMs: transportResult.parseMs,
4759
+ judgeShadowCompared: transportResult.shadowCompared,
4760
+ judgeShadowMismatch: transportResult.shadowMismatch,
4761
+ judgeShadowMismatchRateWindow: transportResult.shadowMismatchRateWindow,
4762
+ judgeKillSwitchTriggered: transportResult.killSwitchTriggered
2866
4763
  };
2867
- return parsed;
4764
+ return transportResult.verdict;
2868
4765
  } catch {
2869
4766
  judge.lastTrace = {
2870
4767
  provider: "fallback",
2871
4768
  modelRequested: options.modelRequested,
2872
4769
  modelResolved: options.modelResolved,
2873
4770
  latencyMs: Date.now() - started,
2874
- fallbackReason: `${providerId}_cli_unavailable`
4771
+ fallbackReason: `${providerId}_cli_unavailable`,
4772
+ judgeFallbackReason: `${providerId}_cli_unavailable`
2875
4773
  };
2876
4774
  return failClosedVerdict2(`${providerId}_cli_unavailable`);
2877
4775
  }
@@ -2912,14 +4810,19 @@ function providerIdFromJudge(judge) {
2912
4810
  }
2913
4811
  return inferProviderIdFromConfig(judge);
2914
4812
  }
2915
- function createCliJudgeForProvider(providerId, judgeConfig, config) {
4813
+ function createCliJudgeForProvider(providerId, judgeConfig, config, repoRoot) {
4814
+ const resolvedRepoRoot = repoRoot ?? process.cwd();
2916
4815
  const { resolved } = resolveJudgeModel(judgeConfig);
2917
4816
  const options = {
2918
4817
  modelRequested: judgeConfig.model,
2919
4818
  modelResolved: resolved,
2920
4819
  timeoutMs: judgeConfig.timeoutMs,
2921
4820
  sensitivePaths: config.classifier.sensitivePaths,
2922
- scrubOptions: scrubOptionsFromConfig(config)
4821
+ scrubOptions: scrubOptionsFromConfig(config),
4822
+ runtime: judgeConfig.runtime,
4823
+ repoRoot: resolvedRepoRoot,
4824
+ stateDir: belayStateDir(config, repoLocalStateDirFor(resolvedRepoRoot, config)),
4825
+ judgeMode: config.mode
2923
4826
  };
2924
4827
  if (providerId === "codex") {
2925
4828
  return createCodexCliJudge(options);
@@ -2948,11 +4851,12 @@ function createJudgeFromConfig(config, options = {}) {
2948
4851
  const catalogSpec = getJudgeProviderSpec(providerId);
2949
4852
  const { resolved } = resolveJudgeModel(judgeConfig);
2950
4853
  const runtime = detectJudgeRuntimeCapabilities(providerId);
4854
+ const repoRoot = options.repoRoot ?? process.cwd();
2951
4855
  if (provider === "openai-compatible") {
2952
4856
  const endpoint = judgeConfig.endpoint?.trim();
2953
4857
  if (!endpoint && runtime.cliTransport) {
2954
4858
  if (providerId === "codex" || providerId === "cursor") {
2955
- return createCliJudgeForProvider(providerId, judgeConfig, config);
4859
+ return createCliJudgeForProvider(providerId, judgeConfig, config, repoRoot);
2956
4860
  }
2957
4861
  }
2958
4862
  if (!endpoint) {
@@ -2971,7 +4875,6 @@ function createJudgeFromConfig(config, options = {}) {
2971
4875
  modelResolved: resolved
2972
4876
  });
2973
4877
  }
2974
- const repoRoot = options.repoRoot ?? process.cwd();
2975
4878
  const repoLocalStateDir = repoLocalStateDirFor(repoRoot, config);
2976
4879
  return createOpenAiCompatibleJudge({
2977
4880
  endpoint,
@@ -3004,7 +4907,11 @@ function createJudgeFromConfig(config, options = {}) {
3004
4907
  modelResolved: resolved,
3005
4908
  timeoutMs: judgeConfig.timeoutMs,
3006
4909
  sensitivePaths: config.classifier.sensitivePaths,
3007
- scrubOptions: scrubOptionsFromConfig(config)
4910
+ scrubOptions: scrubOptionsFromConfig(config),
4911
+ runtime: judgeConfig.runtime,
4912
+ repoRoot,
4913
+ stateDir: belayStateDir(config, repoLocalStateDirFor(repoRoot, config)),
4914
+ judgeMode: config.mode
3008
4915
  });
3009
4916
  }
3010
4917
  return createFailClosedJudge({
@@ -3239,8 +5146,8 @@ function verdictFingerprint(cwdRelative2, commandRedacted) {
3239
5146
  }
3240
5147
 
3241
5148
  // src/core/verdict/launcher-resolve.ts
3242
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
3243
- import path14 from "node:path";
5149
+ import { existsSync as existsSync7, readFileSync as readFileSync3 } from "node:fs";
5150
+ import path19 from "node:path";
3244
5151
  var MAX_RESOLVE_DEPTH = 8;
3245
5152
  var PNPM_BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
3246
5153
  "add",
@@ -3287,28 +5194,28 @@ var PNPM_EXEC_LIKE_HEADS = /* @__PURE__ */ new Set([
3287
5194
  "node"
3288
5195
  ]);
3289
5196
  function readPackageJson(dir) {
3290
- const packagePath = path14.join(dir, "package.json");
3291
- if (!existsSync4(packagePath)) {
5197
+ const packagePath = path19.join(dir, "package.json");
5198
+ if (!existsSync7(packagePath)) {
3292
5199
  return null;
3293
5200
  }
3294
5201
  try {
3295
- return JSON.parse(readFileSync2(packagePath, "utf8"));
5202
+ return JSON.parse(readFileSync3(packagePath, "utf8"));
3296
5203
  } catch {
3297
5204
  return null;
3298
5205
  }
3299
5206
  }
3300
5207
  function findPackageJson(startDir, stopDir) {
3301
- let current = path14.resolve(startDir);
3302
- const stop = path14.resolve(stopDir);
5208
+ let current = path19.resolve(startDir);
5209
+ const stop = path19.resolve(stopDir);
3303
5210
  while (true) {
3304
- const packagePath = path14.join(current, "package.json");
3305
- if (existsSync4(packagePath)) {
5211
+ const packagePath = path19.join(current, "package.json");
5212
+ if (existsSync7(packagePath)) {
3306
5213
  return packagePath;
3307
5214
  }
3308
- if (current === stop || current === path14.dirname(current)) {
3309
- return existsSync4(packagePath) ? packagePath : null;
5215
+ if (current === stop || current === path19.dirname(current)) {
5216
+ return existsSync7(packagePath) ? packagePath : null;
3310
5217
  }
3311
- const parent = path14.dirname(current);
5218
+ const parent = path19.dirname(current);
3312
5219
  if (!parent.startsWith(stop) && parent !== current) {
3313
5220
  }
3314
5221
  if (parent === current) {
@@ -3365,7 +5272,7 @@ function resolveNpmRecipe(cwd, repoRoot, scriptName, extraArgs) {
3365
5272
  }
3366
5273
  return { recipes: [], opaque: true, reason: "package_json_missing" };
3367
5274
  }
3368
- const pkg = readPackageJson(path14.dirname(packagePath));
5275
+ const pkg = readPackageJson(path19.dirname(packagePath));
3369
5276
  const scripts = pkg?.scripts;
3370
5277
  if (!scripts || typeof scripts !== "object") {
3371
5278
  return { recipes: [], opaque: true, reason: "package_scripts_missing" };
@@ -3389,7 +5296,7 @@ function resolveNpmRecipe(cwd, repoRoot, scriptName, extraArgs) {
3389
5296
  function parseMakefileRecipes(makefilePath) {
3390
5297
  const recipes = /* @__PURE__ */ new Map();
3391
5298
  try {
3392
- const content = readFileSync2(makefilePath, "utf8");
5299
+ const content = readFileSync3(makefilePath, "utf8");
3393
5300
  const lines = content.split("\n");
3394
5301
  let currentTarget = null;
3395
5302
  let recipeLines = [];
@@ -3430,20 +5337,20 @@ function parseMakefileRecipes(makefilePath) {
3430
5337
  function resolveMakeRecipe(cwd, repoRoot, target) {
3431
5338
  const candidates = ["Makefile", "makefile", "GNUmakefile"];
3432
5339
  let makefilePath = null;
3433
- let searchDir = path14.resolve(cwd);
3434
- const stop = path14.resolve(repoRoot);
5340
+ let searchDir = path19.resolve(cwd);
5341
+ const stop = path19.resolve(repoRoot);
3435
5342
  while (true) {
3436
5343
  for (const name of candidates) {
3437
- const candidate = path14.join(searchDir, name);
3438
- if (existsSync4(candidate)) {
5344
+ const candidate = path19.join(searchDir, name);
5345
+ if (existsSync7(candidate)) {
3439
5346
  makefilePath = candidate;
3440
5347
  break;
3441
5348
  }
3442
5349
  }
3443
- if (makefilePath || searchDir === stop || searchDir === path14.dirname(searchDir)) {
5350
+ if (makefilePath || searchDir === stop || searchDir === path19.dirname(searchDir)) {
3444
5351
  break;
3445
5352
  }
3446
- searchDir = path14.dirname(searchDir);
5353
+ searchDir = path19.dirname(searchDir);
3447
5354
  }
3448
5355
  if (!makefilePath) {
3449
5356
  return { recipes: [], opaque: true, reason: "unknown_local_effect" };
@@ -3547,7 +5454,7 @@ function askFromCustomExternal(opacity) {
3547
5454
  }
3548
5455
 
3549
5456
  // src/core/verdict/parser.ts
3550
- import path15 from "node:path";
5457
+ import path20 from "node:path";
3551
5458
 
3552
5459
  // src/core/shell-substitution.ts
3553
5460
  function findCommandSubstitutions(command) {
@@ -3797,7 +5704,7 @@ var INTERPRETER_SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
3797
5704
  ".sh"
3798
5705
  ]);
3799
5706
  function normalizeHead(token) {
3800
- const base = path15.basename(token);
5707
+ const base = path20.basename(token);
3801
5708
  if (base && base !== "." && base !== "..") {
3802
5709
  return base;
3803
5710
  }
@@ -3902,7 +5809,7 @@ function isBareInterpreter(tokens) {
3902
5809
  return false;
3903
5810
  }
3904
5811
  const scriptArg = args.find((token) => !token.startsWith("-"));
3905
- if (scriptArg && INTERPRETER_SCRIPT_EXTENSIONS.has(path15.extname(scriptArg))) {
5812
+ if (scriptArg && INTERPRETER_SCRIPT_EXTENSIONS.has(path20.extname(scriptArg))) {
3906
5813
  return false;
3907
5814
  }
3908
5815
  if (scriptArg) {
@@ -3921,7 +5828,7 @@ function splitTopLevelSegments(command) {
3921
5828
  }
3922
5829
  };
3923
5830
  for (const token of tokens) {
3924
- if (token === "&&" || token === "||" || token === ";" || token === "|" || token === "&") {
5831
+ if (token === "&&" || token === "||" || token === ";" || token === "|" || token === "&" || token === "|&") {
3925
5832
  flush();
3926
5833
  continue;
3927
5834
  }
@@ -4143,7 +6050,7 @@ function extractPathArgs(tokens) {
4143
6050
  const args = [...redirects];
4144
6051
  for (let index = 1; index < tokens.length; index += 1) {
4145
6052
  const token = tokens[index];
4146
- if (!token || token.startsWith("-") || token === ">" || token === ">>" || token === "<") {
6053
+ if (!token || token.startsWith("-") || isRedirectOperator(token) || isFdDuplication(token)) {
4147
6054
  continue;
4148
6055
  }
4149
6056
  if (redirects.includes(token)) {
@@ -4240,6 +6147,7 @@ function tier0HighStakesRm(tokens, context) {
4240
6147
  cwd: context.cwd,
4241
6148
  repoRoot: context.repoRoot,
4242
6149
  trustedCwd: context.trustedCwd,
6150
+ trustedWorkspaceRoots: context.trustedWorkspaceRoots,
4243
6151
  sensitivePaths: context.sensitivePaths,
4244
6152
  protectedArtifactRoots: context.protectedArtifactRoots
4245
6153
  });
@@ -4275,7 +6183,8 @@ function tier0HighStakesRm(tokens, context) {
4275
6183
  resolved,
4276
6184
  context.repoRoot,
4277
6185
  context.sensitivePaths,
4278
- context.protectedArtifactRoots
6186
+ context.protectedArtifactRoots,
6187
+ context.trustedWorkspaceRoots
4279
6188
  )) {
4280
6189
  destructiveHighStakes = true;
4281
6190
  break;
@@ -4497,6 +6406,7 @@ async function evaluateSegment(command, context, depth) {
4497
6406
  cwd: context.cwd,
4498
6407
  repoRoot: context.repoRoot,
4499
6408
  trustedCwd: context.trustedCwd,
6409
+ trustedWorkspaceRoots: context.trustedWorkspaceRoots,
4500
6410
  sensitivePaths: context.sensitivePaths,
4501
6411
  protectedArtifactRoots: context.protectedArtifactRoots
4502
6412
  });
@@ -4546,7 +6456,8 @@ async function evaluateSegment(command, context, depth) {
4546
6456
  resolved,
4547
6457
  context.repoRoot,
4548
6458
  context.sensitivePaths,
4549
- context.protectedArtifactRoots
6459
+ context.protectedArtifactRoots,
6460
+ context.trustedWorkspaceRoots
4550
6461
  )) {
4551
6462
  destructiveHighStakes = true;
4552
6463
  break;
@@ -4599,6 +6510,7 @@ async function evaluateSegment(command, context, depth) {
4599
6510
  cwd: context.cwd,
4600
6511
  repoRoot: context.repoRoot,
4601
6512
  trustedCwd: context.trustedCwd,
6513
+ trustedWorkspaceRoots: context.trustedWorkspaceRoots,
4602
6514
  sensitivePaths: context.sensitivePaths
4603
6515
  }) : null;
4604
6516
  if (mutationPrescan) {
@@ -4780,11 +6692,12 @@ function buildVerdictContext(params) {
4780
6692
  cwd: params.cwd,
4781
6693
  repoRoot: params.repoRoot,
4782
6694
  trustedCwd: resolveClassifierTrustedCwd(params.cwd, params.options, params.trustedCwd),
6695
+ trustedWorkspaceRoots: params.options?.trustedWorkspaceRoots ?? [],
4783
6696
  sensitivePaths: params.options?.sensitivePaths ?? params.config.classifier.sensitivePaths,
4784
6697
  protectedArtifactRoots: protectedArtifactRoots2.length > 0 ? [...new Set(protectedArtifactRoots2)] : void 0,
4785
6698
  customAllowCommands: params.options?.customAllowCommands ?? params.config.overrides.allow,
4786
6699
  customExternalCommands: params.options?.customExternalCommands ?? params.config.overrides.external,
4787
- judge: params.judge ?? params.options?.tier1Judge ?? createJudgeFromConfig(params.config),
6700
+ judge: params.judge ?? params.options?.tier1Judge ?? createJudgeFromConfig(params.config, { repoRoot: params.repoRoot }),
4788
6701
  mode: params.config.mode,
4789
6702
  unknownLocalEffect: params.options?.unknownLocalEffect ?? params.config.policy.unknownLocalEffect,
4790
6703
  unparseableShell: params.options?.unparseableShell ?? params.config.policy.unparseableShell
@@ -4792,7 +6705,12 @@ function buildVerdictContext(params) {
4792
6705
  }
4793
6706
  async function classifyShell(command, cwd, repoRoot, config, options = {}, judge) {
4794
6707
  const context = buildVerdictContext({ cwd, repoRoot, config, options, judge });
6708
+ const started = Date.now();
4795
6709
  const result = await verdict(command, context);
6710
+ const elapsed = Date.now() - started;
6711
+ if (result.confidence !== "llm") {
6712
+ recordJudgeLatency("tier0", elapsed);
6713
+ }
4796
6714
  return verdictToClassifyResult(result);
4797
6715
  }
4798
6716
  function mapLegacyReason(result) {
@@ -4940,6 +6858,7 @@ async function classifyFileMutationWithTier1(params) {
4940
6858
  cwd: params.cwd,
4941
6859
  repoRoot: params.repoRoot,
4942
6860
  trustedCwd,
6861
+ trustedWorkspaceRoots: params.options.trustedWorkspaceRoots,
4943
6862
  sensitivePaths: params.options.sensitivePaths ?? params.config.classifier.sensitivePaths
4944
6863
  });
4945
6864
  if (prescan) {
@@ -5088,7 +7007,7 @@ async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
5088
7007
  };
5089
7008
  }
5090
7009
  const signals = [];
5091
- const resolvedPath = path16.isAbsolute(filePath) ? filePath : path16.resolve(cwd, filePath);
7010
+ const resolvedPath = path21.isAbsolute(filePath) ? filePath : path21.resolve(cwd, filePath);
5092
7011
  const hitsProtectedRoot = protectedRoots.some((root) => pathWithinRoot(root, resolvedPath));
5093
7012
  if (hitsProtectedRoot) {
5094
7013
  signals.push("control_plane_path");
@@ -5106,8 +7025,12 @@ async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
5106
7025
  }
5107
7026
  };
5108
7027
  }
5109
- const relativePath = relativeWithinRepo(repoRoot, resolvedPath);
5110
- if (relativePath === null) {
7028
+ const workspaceMatch = resolveWorkspaceRootMatch(
7029
+ repoRoot,
7030
+ options.trustedWorkspaceRoots,
7031
+ resolvedPath
7032
+ );
7033
+ if (workspaceMatch === null) {
5111
7034
  signals.push("outside_repo_path");
5112
7035
  return classifyFileMutationWithTier1({
5113
7036
  toolName,
@@ -5123,6 +7046,34 @@ async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
5123
7046
  locationLabel: "outside_repo"
5124
7047
  });
5125
7048
  }
7049
+ if (workspaceMatch.kind === "trusted") {
7050
+ signals.push("trusted_workspace_root");
7051
+ }
7052
+ const trustedCwd = resolveClassifierTrustedCwd(cwd, options);
7053
+ const workspacePrescan = mutationPrescanRequiresAsk({
7054
+ targets: [filePath],
7055
+ cwd,
7056
+ repoRoot,
7057
+ trustedCwd,
7058
+ trustedWorkspaceRoots: options.trustedWorkspaceRoots,
7059
+ sensitivePaths
7060
+ });
7061
+ if (workspacePrescan) {
7062
+ return classifyFileMutationWithTier1({
7063
+ toolName,
7064
+ toolKind,
7065
+ filePath,
7066
+ resolvedPath,
7067
+ repoRoot,
7068
+ cwd,
7069
+ config,
7070
+ options,
7071
+ signals,
7072
+ isDelete: FILE_DELETE_TOOL_NAMES.has(toolKind),
7073
+ locationLabel: "sensitive_path"
7074
+ });
7075
+ }
7076
+ const relativePath = workspaceMatch.relativePath;
5126
7077
  if (matchesSensitivePath(relativePath, sensitivePaths)) {
5127
7078
  signals.push("sensitive_path");
5128
7079
  return classifyFileMutationWithTier1({
@@ -5430,30 +7381,50 @@ function applyFsScopePeripheralPolicy(outsideRepoPaths, result, options) {
5430
7381
  return result;
5431
7382
  }
5432
7383
  function applySandboxOutsideBoundary(command, action, result, options) {
5433
- const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot);
7384
+ const outsideRepoPaths = collectOutsideRepoPaths(
7385
+ command,
7386
+ action.cwd,
7387
+ action.repoRoot,
7388
+ options.trustedWorkspaceRoots
7389
+ );
5434
7390
  return applySandboxFsScopeBoundary(outsideRepoPaths, result, options, {
5435
7391
  redirect: command.includes(">")
5436
7392
  });
5437
7393
  }
5438
7394
  function applyShellPeripheralPolicy(command, action, result, options) {
5439
- const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot);
7395
+ const outsideRepoPaths = collectOutsideRepoPaths(
7396
+ command,
7397
+ action.cwd,
7398
+ action.repoRoot,
7399
+ options.trustedWorkspaceRoots
7400
+ );
5440
7401
  return applyFsScopePeripheralPolicy(outsideRepoPaths, result, options);
5441
7402
  }
5442
- function outsideRepoPathsForToolAction(action) {
7403
+ function outsideRepoPathsForToolAction(action, options) {
5443
7404
  const payload = action.payload ?? {};
5444
7405
  const paths = new Set(
5445
- collectOutsideRepoPathsFromToolPayload(payload, action.cwd, action.repoRoot)
7406
+ collectOutsideRepoPathsFromToolPayload(
7407
+ payload,
7408
+ action.cwd,
7409
+ action.repoRoot,
7410
+ options.trustedWorkspaceRoots
7411
+ )
5446
7412
  );
5447
7413
  const command = shellCommandFromPayload(payload);
5448
7414
  if (command) {
5449
- for (const resolved of collectOutsideRepoPaths(command, action.cwd, action.repoRoot)) {
7415
+ for (const resolved of collectOutsideRepoPaths(
7416
+ command,
7417
+ action.cwd,
7418
+ action.repoRoot,
7419
+ options.trustedWorkspaceRoots
7420
+ )) {
5450
7421
  paths.add(resolved);
5451
7422
  }
5452
7423
  }
5453
7424
  return [...paths];
5454
7425
  }
5455
7426
  function applyToolSandboxPolicies(action, result, options) {
5456
- const outsideRepoPaths = outsideRepoPathsForToolAction(action);
7427
+ const outsideRepoPaths = outsideRepoPathsForToolAction(action, options);
5457
7428
  const command = shellCommandFromPayload(action.payload ?? {});
5458
7429
  let next = applySandboxFsScopeBoundary(outsideRepoPaths, result, options, {
5459
7430
  redirect: command.includes(">")
@@ -5534,9 +7505,9 @@ function gateEnabledForAction(config, action) {
5534
7505
  }
5535
7506
 
5536
7507
  // src/core/transactional/diff-evaluator.ts
5537
- import path17 from "node:path";
7508
+ import path22 from "node:path";
5538
7509
  function categorizeChange(change, ctx) {
5539
- const absolutePath = canonicalPath(path17.join(ctx.repoRoot, change.relativePath));
7510
+ const absolutePath = canonicalPath(path22.join(ctx.repoRoot, change.relativePath));
5540
7511
  if (!pathWithinRoot(ctx.repoRoot, absolutePath)) {
5541
7512
  return "repo_outside";
5542
7513
  }
@@ -5656,15 +7627,15 @@ var TRANSACTIONAL_APPROVAL_BYPASS_REASONS = /* @__PURE__ */ new Set([
5656
7627
  ]);
5657
7628
 
5658
7629
  // src/core/transactional/git-worktree.ts
5659
- import { spawn as spawn2 } from "node:child_process";
7630
+ import { spawn as spawn5 } from "node:child_process";
5660
7631
  import { randomUUID } from "node:crypto";
5661
- import { existsSync as existsSync5 } from "node:fs";
5662
- import { copyFile, mkdir as mkdir3, mkdtemp, rm } from "node:fs/promises";
7632
+ import { existsSync as existsSync8 } from "node:fs";
7633
+ import { copyFile, mkdir as mkdir4, mkdtemp, rm } from "node:fs/promises";
5663
7634
  import os from "node:os";
5664
- import path18 from "node:path";
7635
+ import path23 from "node:path";
5665
7636
  function execGit(repoRoot, args) {
5666
7637
  return new Promise((resolve, reject) => {
5667
- const child = spawn2("git", ["-C", repoRoot, ...args], {
7638
+ const child = spawn5("git", ["-C", repoRoot, ...args], {
5668
7639
  stdio: ["ignore", "pipe", "pipe"]
5669
7640
  });
5670
7641
  const stdout = [];
@@ -5702,8 +7673,8 @@ async function isDirtyWorktree(repoRoot) {
5702
7673
  }
5703
7674
  }
5704
7675
  async function createGitWorktreeSnapshot(repoRoot, stateDir) {
5705
- const worktreePath = path18.join(stateDir, `tx-${randomUUID().replaceAll("-", "")}`);
5706
- await mkdir3(stateDir, { recursive: true });
7676
+ const worktreePath = path23.join(stateDir, `tx-${randomUUID().replaceAll("-", "")}`);
7677
+ await mkdir4(stateDir, { recursive: true });
5707
7678
  await execGit(repoRoot, ["worktree", "add", "--detach", worktreePath, "HEAD"]);
5708
7679
  return {
5709
7680
  worktreePath,
@@ -5722,18 +7693,18 @@ async function createGitWorktreeSnapshot(repoRoot, stateDir) {
5722
7693
  }
5723
7694
  function resolveWorktreeCwd(repoRoot, worktreePath, cwd) {
5724
7695
  const resolvedCwd = canonicalPath(cwd);
5725
- const relative = path18.relative(canonicalPath(repoRoot), resolvedCwd);
5726
- if (relative.startsWith("..") || path18.isAbsolute(relative)) {
7696
+ const relative = path23.relative(canonicalPath(repoRoot), resolvedCwd);
7697
+ if (relative.startsWith("..") || path23.isAbsolute(relative)) {
5727
7698
  return worktreePath;
5728
7699
  }
5729
7700
  if (relative === "") {
5730
7701
  return worktreePath;
5731
7702
  }
5732
- return path18.join(worktreePath, relative);
7703
+ return path23.join(worktreePath, relative);
5733
7704
  }
5734
7705
  function runShellCommand(command, cwd, timeoutMs) {
5735
7706
  return new Promise((resolve) => {
5736
- const child = spawn2(command, {
7707
+ const child = spawn5(command, {
5737
7708
  cwd,
5738
7709
  shell: true,
5739
7710
  stdio: "ignore",
@@ -5799,7 +7770,7 @@ async function rollbackAppliedChanges(actions) {
5799
7770
  for (const action of [...actions].reverse()) {
5800
7771
  try {
5801
7772
  if (action.type === "restore") {
5802
- await mkdir3(path18.dirname(action.target), { recursive: true });
7773
+ await mkdir4(path23.dirname(action.target), { recursive: true });
5803
7774
  await copyFile(action.backupPath, action.target);
5804
7775
  } else {
5805
7776
  await rm(action.target, { force: true });
@@ -5809,14 +7780,14 @@ async function rollbackAppliedChanges(actions) {
5809
7780
  }
5810
7781
  }
5811
7782
  async function applyWorktreeChanges(worktreePath, repoRoot, changes) {
5812
- const backupRoot = await mkdtemp(path18.join(os.tmpdir(), "belay-tx-rollback-"));
7783
+ const backupRoot = await mkdtemp(path23.join(os.tmpdir(), "belay-tx-rollback-"));
5813
7784
  const rollbackActions = [];
5814
7785
  try {
5815
7786
  for (const change of changes) {
5816
- const target = path18.join(repoRoot, change.relativePath);
5817
- if (existsSync5(target)) {
5818
- const backupPath = path18.join(backupRoot, change.relativePath);
5819
- await mkdir3(path18.dirname(backupPath), { recursive: true });
7787
+ const target = path23.join(repoRoot, change.relativePath);
7788
+ if (existsSync8(target)) {
7789
+ const backupPath = path23.join(backupRoot, change.relativePath);
7790
+ await mkdir4(path23.dirname(backupPath), { recursive: true });
5820
7791
  await copyFile(target, backupPath);
5821
7792
  rollbackActions.push({ type: "restore", target, backupPath });
5822
7793
  } else if (change.kind !== "deleted") {
@@ -5826,8 +7797,8 @@ async function applyWorktreeChanges(worktreePath, repoRoot, changes) {
5826
7797
  await rm(target, { force: true });
5827
7798
  continue;
5828
7799
  }
5829
- const source = path18.join(worktreePath, change.relativePath);
5830
- await mkdir3(path18.dirname(target), { recursive: true });
7800
+ const source = path23.join(worktreePath, change.relativePath);
7801
+ await mkdir4(path23.dirname(target), { recursive: true });
5831
7802
  await copyFile(source, target);
5832
7803
  }
5833
7804
  } catch (error) {
@@ -5988,8 +7959,225 @@ async function notifyDeny(config, event) {
5988
7959
  }
5989
7960
  }
5990
7961
 
7962
+ // src/core/standing-allow.ts
7963
+ import { existsSync as existsSync9 } from "node:fs";
7964
+ import { mkdir as mkdir5, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
7965
+ import path24 from "node:path";
7966
+
7967
+ // src/corpus/standing-allow-catalog.generated.ts
7968
+ var STANDING_ALLOW_CATALOG = {
7969
+ version: 1,
7970
+ shell: {
7971
+ provablyBenign: [
7972
+ {
7973
+ command: "git status",
7974
+ normalizedCommand: "git status",
7975
+ runtimeKey: "37e6be21e63e6268ee6e4212ad811f6f48ab3c3c10ce1b971c1a90d92c753a2a"
7976
+ },
7977
+ {
7978
+ command: "rg plan src",
7979
+ normalizedCommand: "rg plan src",
7980
+ runtimeKey: "68ae7a96158719235d9314da7b00a5f8b2f40668e5d4f7f02e91f22284140a68"
7981
+ },
7982
+ {
7983
+ command: "ls -la",
7984
+ normalizedCommand: "ls -la",
7985
+ runtimeKey: "3a665991b52b5f18069889960b957ea37b087859c23e2c87b76ef8d2a1b338fe"
7986
+ },
7987
+ {
7988
+ command: "find . -name '*.ts'",
7989
+ normalizedCommand: "find . -name '*.ts'",
7990
+ runtimeKey: "782298e3015fcb9c17bee2dad12a395a8859043481f93f0ca596499028bb8af2"
7991
+ },
7992
+ {
7993
+ command: "curl https://example.com",
7994
+ normalizedCommand: "curl https://example.com",
7995
+ runtimeKey: "09fa1bf897aa68a2d0be02cff3b0cbfa44a28fa17c501e8a39f2081208785e55"
7996
+ },
7997
+ {
7998
+ command: "wget https://example.com",
7999
+ normalizedCommand: "wget https://example.com",
8000
+ runtimeKey: "c408836b4bb4498b68b52e08a7c852757a19e156cae7579ffd5c98f13728cacb"
8001
+ },
8002
+ {
8003
+ command: "ls\ncurl https://example.com",
8004
+ normalizedCommand: "ls\ncurl https://example.com",
8005
+ runtimeKey: "a895f27dda259649bbe0d27fcc04ab7076fadfcd4c2ea3ab55f557028b01b11f"
8006
+ }
8007
+ ],
8008
+ mustAllow: [
8009
+ {
8010
+ command: "npm test",
8011
+ normalizedCommand: "npm test",
8012
+ runtimeKey: "1bcafcfef8e1b0138297885032c5e222c4aed19bdcde00e2539aa397fdc877cb"
8013
+ },
8014
+ {
8015
+ command: "npm run build",
8016
+ normalizedCommand: "npm run build",
8017
+ runtimeKey: "3138d8a3e9afb60574e29d16c2967f70bb42475e02eadb261f5f2340f376c188"
8018
+ },
8019
+ {
8020
+ command: "pnpm test",
8021
+ normalizedCommand: "pnpm test",
8022
+ runtimeKey: "eaecc988be421de36044932e32d4be807e45baccaa94c2d29d30a34204656551"
8023
+ },
8024
+ {
8025
+ command: "pnpm build",
8026
+ normalizedCommand: "pnpm build",
8027
+ runtimeKey: "a1ec94b6b1f7f17ab0fe468e41c6dc59e04266d34772abcaa9538123601cb46d"
8028
+ },
8029
+ {
8030
+ command: "pnpm vitest run src/example.test.ts",
8031
+ normalizedCommand: "pnpm vitest run src/example.test.ts",
8032
+ runtimeKey: "011e12e902154e7cf598f390fc08a50e885b34893deb97d242cf7470e23e617e"
8033
+ },
8034
+ {
8035
+ command: "bash -lc 'git status'",
8036
+ normalizedCommand: "bash -lc 'git status'",
8037
+ runtimeKey: "dbcc63715f4a8cca2aff988470bb1d52a4841542af9c86776ff2414509508a1f"
8038
+ },
8039
+ {
8040
+ command: "belay approve belay_deadbeef1234",
8041
+ normalizedCommand: "belay approve belay_deadbeef1234",
8042
+ runtimeKey: "bb3f0f1dac3d6c4ef79e17ef4e36f9430195596490bb736cdb8b25a5944d6f81"
8043
+ }
8044
+ ]
8045
+ }
8046
+ };
8047
+
8048
+ // src/core/standing-allow.ts
8049
+ var EMPTY_STANDING_ALLOW = {
8050
+ version: 1,
8051
+ entries: []
8052
+ };
8053
+ var DEFAULT_STANDING_ALLOW_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
8054
+ var PROVABLY_BENIGN_COMMANDS = new Set(
8055
+ STANDING_ALLOW_CATALOG.shell.provablyBenign.map((entry) => entry.normalizedCommand)
8056
+ );
8057
+ var MUST_ALLOW_COMMANDS = new Set(
8058
+ STANDING_ALLOW_CATALOG.shell.mustAllow.map((entry) => entry.normalizedCommand)
8059
+ );
8060
+ var STANDING_ALLOW_BLOCKED_REASONS = /* @__PURE__ */ new Set([
8061
+ "external_effect",
8062
+ "tier1_catastrophic",
8063
+ "protected_artifact",
8064
+ "pipe_to_shell",
8065
+ "command_substitution"
8066
+ ]);
8067
+ function standingAllowFile(config, repoLocalStateDir) {
8068
+ return `${belayStateDir(config, repoLocalStateDir)}/standing-allow.json`;
8069
+ }
8070
+ function isTier0StandingAllowBlocked(result) {
8071
+ if (result.reason.startsWith("tier0_") || STANDING_ALLOW_BLOCKED_REASONS.has(result.reason)) {
8072
+ return true;
8073
+ }
8074
+ const signals = result.assessment?.signals ?? [];
8075
+ return signals.some((signal) => signal === "tier0_external" || signal === "tier1_catastrophic");
8076
+ }
8077
+ function isExpired2(iso, now = Date.now()) {
8078
+ const parsed = Date.parse(iso);
8079
+ return Number.isNaN(parsed) || parsed <= now;
8080
+ }
8081
+ function compactStandingAllow(state, now = Date.now()) {
8082
+ return {
8083
+ version: 1,
8084
+ entries: state.entries.filter((entry) => !isExpired2(entry.expiresAt, now))
8085
+ };
8086
+ }
8087
+ function sanitizeStandingAllowEntries(input) {
8088
+ if (!Array.isArray(input)) {
8089
+ return [];
8090
+ }
8091
+ return input.flatMap((entry) => {
8092
+ if (!entry || typeof entry !== "object") {
8093
+ return [];
8094
+ }
8095
+ const record = entry;
8096
+ if (record.kind !== "shell" && record.kind !== "tool" && record.kind !== "subagent" || typeof record.fingerprint !== "string" || !record.fingerprint.trim() || typeof record.expiresAt !== "string" || typeof record.createdAt !== "string" || typeof record.reason !== "string") {
8097
+ return [];
8098
+ }
8099
+ const source = record.source;
8100
+ if (source !== "operator" && source !== "availability-reconfirmed") {
8101
+ return [];
8102
+ }
8103
+ return [
8104
+ {
8105
+ kind: record.kind,
8106
+ fingerprint: record.fingerprint,
8107
+ source,
8108
+ reason: record.reason,
8109
+ createdAt: record.createdAt,
8110
+ expiresAt: record.expiresAt,
8111
+ ...typeof record.summary === "string" ? { summary: record.summary } : {},
8112
+ ...typeof record.repoRoot === "string" ? { repoRoot: record.repoRoot } : {}
8113
+ }
8114
+ ];
8115
+ });
8116
+ }
8117
+ async function loadStandingAllow(filePath) {
8118
+ if (!existsSync9(filePath)) {
8119
+ return { ...EMPTY_STANDING_ALLOW };
8120
+ }
8121
+ const raw = JSON.parse(await readFile4(filePath, "utf8"));
8122
+ const rawCount = Array.isArray(raw.entries) ? raw.entries.length : 0;
8123
+ const entries = sanitizeStandingAllowEntries(raw.entries);
8124
+ const compacted = compactStandingAllow({ version: 1, entries });
8125
+ if (rawCount !== compacted.entries.length || entries.length !== compacted.entries.length) {
8126
+ await saveStandingAllow(filePath, compacted);
8127
+ }
8128
+ return compacted;
8129
+ }
8130
+ async function saveStandingAllow(filePath, state) {
8131
+ await mkdir5(path24.dirname(filePath), { recursive: true });
8132
+ const compacted = compactStandingAllow(state);
8133
+ await writeFile4(filePath, `${JSON.stringify(compacted, null, 2)}
8134
+ `, "utf8");
8135
+ }
8136
+ function matchBundledCatalog(kind, normalizedCommand) {
8137
+ if (kind !== "shell" || !normalizedCommand) {
8138
+ return null;
8139
+ }
8140
+ if (PROVABLY_BENIGN_COMMANDS.has(normalizedCommand)) {
8141
+ return { source: "provably-benign-corpus", catalogCommand: normalizedCommand };
8142
+ }
8143
+ if (MUST_ALLOW_COMMANDS.has(normalizedCommand)) {
8144
+ return { source: "must-allow-catalog", catalogCommand: normalizedCommand };
8145
+ }
8146
+ return null;
8147
+ }
8148
+ function matchStateEntry(params) {
8149
+ const now = params.now ?? Date.now();
8150
+ const match = params.state.entries.find(
8151
+ (entry) => entry.kind === params.kind && entry.fingerprint === params.fingerprint && !isExpired2(entry.expiresAt, now) && (entry.repoRoot === void 0 || entry.repoRoot === params.repoRoot)
8152
+ );
8153
+ if (!match) {
8154
+ return null;
8155
+ }
8156
+ return { source: match.source, entryId: match.fingerprint };
8157
+ }
8158
+ function resolveStandingAllowMatch(params) {
8159
+ if (params.result.verdict !== "deny_pending_approval") {
8160
+ return null;
8161
+ }
8162
+ if (isTier0StandingAllowBlocked(params.result)) {
8163
+ return null;
8164
+ }
8165
+ const normalizedCommand = params.result.normalizedCommand ?? params.result.summary ?? "";
8166
+ const catalogMatch = matchBundledCatalog(params.kind, normalizedCommand);
8167
+ if (catalogMatch) {
8168
+ return catalogMatch;
8169
+ }
8170
+ return matchStateEntry({
8171
+ kind: params.kind,
8172
+ fingerprint: params.result.fingerprint,
8173
+ repoRoot: params.repoRoot,
8174
+ state: params.state,
8175
+ now: params.now
8176
+ });
8177
+ }
8178
+
5991
8179
  // src/adapters/layouts/protected-paths.ts
5992
- import path19 from "node:path";
8180
+ import path25 from "node:path";
5993
8181
  function protectedArtifactRoots(layout, repoRoot, controlPlaneDir) {
5994
8182
  const roots = [
5995
8183
  layout.configPath(repoRoot),
@@ -6001,7 +8189,7 @@ function protectedArtifactRoots(layout, repoRoot, controlPlaneDir) {
6001
8189
  if (controlPlaneDir) {
6002
8190
  roots.push(controlPlaneDir);
6003
8191
  }
6004
- return roots.map((entry) => path19.resolve(entry));
8192
+ return roots.map((entry) => path25.resolve(entry));
6005
8193
  }
6006
8194
 
6007
8195
  // src/adapters/shared/gate-runtime.ts
@@ -6009,9 +8197,18 @@ var EMPTY_APPROVALS = {
6009
8197
  version: 1,
6010
8198
  approvals: []
6011
8199
  };
8200
+ function adapterIdFromContext(ctx) {
8201
+ if (ctx.config.adapter === "cursor" || ctx.config.adapter === "claude" || ctx.config.adapter === "codex") {
8202
+ return ctx.config.adapter;
8203
+ }
8204
+ if (ctx.layout.name === "cursor" || ctx.layout.name === "claude" || ctx.layout.name === "codex") {
8205
+ return ctx.layout.name;
8206
+ }
8207
+ return void 0;
8208
+ }
6012
8209
  async function loadJsonFile(filePath, fallback) {
6013
8210
  try {
6014
- const raw = await readFile3(filePath, "utf8");
8211
+ const raw = await readFile5(filePath, "utf8");
6015
8212
  return JSON.parse(raw);
6016
8213
  } catch {
6017
8214
  return fallback;
@@ -6023,14 +8220,14 @@ function createDefaultGateRuntimeDeps() {
6023
8220
  return loadJsonFile(configPath, {});
6024
8221
  },
6025
8222
  async appendAudit(ctx, event) {
6026
- const auditPath = path20.join(ctx.repoRoot, ctx.config.audit.logPath);
6027
- await mkdir4(path20.dirname(auditPath), { recursive: true });
8223
+ const auditPath = path26.join(ctx.repoRoot, ctx.config.audit.logPath);
8224
+ await mkdir6(path26.dirname(auditPath), { recursive: true });
6028
8225
  const record = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), ...event };
6029
8226
  if (!ctx.config.audit.includeAssessment) {
6030
8227
  delete record.assessment;
6031
8228
  }
6032
8229
  const scrubbed = scrubValue(record, scrubOptionsFromConfig(ctx.config));
6033
- await writeFile3(auditPath, `${JSON.stringify(scrubbed)}
8230
+ await writeFile5(auditPath, `${JSON.stringify(scrubbed)}
6034
8231
  `, {
6035
8232
  encoding: "utf8",
6036
8233
  flag: "a"
@@ -6049,8 +8246,8 @@ function createDefaultGateRuntimeDeps() {
6049
8246
  };
6050
8247
  },
6051
8248
  async writeApprovals(filePath, state) {
6052
- await mkdir4(path20.dirname(filePath), { recursive: true });
6053
- await writeFile3(filePath, `${JSON.stringify(compactApprovals(state), null, 2)}
8249
+ await mkdir6(path26.dirname(filePath), { recursive: true });
8250
+ await writeFile5(filePath, `${JSON.stringify(compactApprovals(state), null, 2)}
6054
8251
  `, "utf8");
6055
8252
  }
6056
8253
  };
@@ -6059,8 +8256,8 @@ async function resolveGateConfig(ctx, deps) {
6059
8256
  const loaded = await deps.readConfig(ctx.configPath);
6060
8257
  let teamConfig = null;
6061
8258
  const teamPath = teamConfigPath();
6062
- if (existsSync6(teamPath)) {
6063
- teamConfig = JSON.parse(await readFile3(teamPath, "utf8"));
8259
+ if (existsSync10(teamPath)) {
8260
+ teamConfig = JSON.parse(await readFile5(teamPath, "utf8"));
6064
8261
  }
6065
8262
  return resolveLayeredConfig({
6066
8263
  repoConfig: loaded,
@@ -6082,9 +8279,13 @@ function repoShellClassifierOptions(config, repoRoot, layout, extras = {}) {
6082
8279
  function runtimeClassifierOptions(ctx, config) {
6083
8280
  const repoLocalStateDir = ctx.layout.repoLocalStateDir(ctx.repoRoot);
6084
8281
  const brokerFsScope = isCapabilityBrokerDemotionActive(config);
8282
+ const trustedRoots = loadTrustedWorkspaceRootsSync(
8283
+ trustedWorkspaceRootsPath(config, repoLocalStateDir)
8284
+ );
6085
8285
  return repoShellClassifierOptions(config, ctx.repoRoot, ctx.layout, {
6086
8286
  brokerFsScope,
6087
- fsScopeAllowlist: brokerFsScope ? loadFsScopeAllowlistSync(fsScopeAllowlistPath(config, repoLocalStateDir)) : void 0
8287
+ fsScopeAllowlist: brokerFsScope ? loadFsScopeAllowlistSync(fsScopeAllowlistPath(config, repoLocalStateDir)) : void 0,
8288
+ trustedWorkspaceRoots: trustedRoots.roots.map((entry) => entry.path)
6088
8289
  });
6089
8290
  }
6090
8291
  function gateAuditEventName(kind) {
@@ -6096,7 +8297,7 @@ function gateAuditEventName(kind) {
6096
8297
  }
6097
8298
  return "subagentGate";
6098
8299
  }
6099
- async function ensurePendingApproval(ctx, deps, kind, result, approvalInput) {
8300
+ async function ensurePendingApproval(ctx, deps, kind, result, approvalInput, scopeHint) {
6100
8301
  const pending = await deps.loadApprovals(ctx, "pending-approvals.json");
6101
8302
  pending.state = compactApprovals(pending.state);
6102
8303
  const existing = pending.state.approvals.find(
@@ -6106,7 +8307,7 @@ async function ensurePendingApproval(ctx, deps, kind, result, approvalInput) {
6106
8307
  await deps.writeApprovals(pending.filePath, pending.state);
6107
8308
  return existing;
6108
8309
  }
6109
- const approval = createApprovalRecord({
8310
+ const approval = createApprovalRecordWithEnvelope({
6110
8311
  kind,
6111
8312
  fingerprint: result.fingerprint,
6112
8313
  repoRoot: ctx.repoRoot,
@@ -6114,14 +8315,83 @@ async function ensurePendingApproval(ctx, deps, kind, result, approvalInput) {
6114
8315
  summary: result.normalizedCommand ?? result.summary ?? "",
6115
8316
  approvalTtlMinutes: ctx.config.approvalTtlMinutes,
6116
8317
  approvalId: `belay_${randomUUID2().replaceAll("-", "").slice(0, 12)}`,
6117
- input: approvalInput?.input,
6118
- inputKind: approvalInput?.inputKind
8318
+ approvalInput,
8319
+ scopeHint
6119
8320
  });
6120
8321
  pending.state.version = 2;
6121
8322
  pending.state.approvals.push(approval);
6122
8323
  await deps.writeApprovals(pending.filePath, pending.state);
6123
8324
  return approval;
6124
8325
  }
8326
+ function extractShellCommandFromPayload(payload) {
8327
+ const toolInput = payload.tool_input;
8328
+ if (!toolInput || typeof toolInput !== "object") {
8329
+ return "";
8330
+ }
8331
+ const input = toolInput;
8332
+ return typeof input.command === "string" ? input.command : "";
8333
+ }
8334
+ function deriveWorkspaceRootScopeHint(params) {
8335
+ if (params.result.reason !== "outside_repo_mutation" && params.result.reason !== "outside_repo_redirect") {
8336
+ return void 0;
8337
+ }
8338
+ const action = params.replayAction;
8339
+ if (!action?.cwd) {
8340
+ return void 0;
8341
+ }
8342
+ const outsidePaths = /* @__PURE__ */ new Set();
8343
+ if (action.kind === "shell" && action.command) {
8344
+ for (const resolved of collectOutsideRepoPaths(
8345
+ action.command,
8346
+ action.cwd,
8347
+ action.repoRoot,
8348
+ params.options.trustedWorkspaceRoots
8349
+ )) {
8350
+ outsidePaths.add(resolved);
8351
+ }
8352
+ } else if (action.kind === "tool") {
8353
+ const toolPayload = params.payloadForScopeHint ?? action.payload ?? {};
8354
+ for (const resolved of collectOutsideRepoPathsFromToolPayload(
8355
+ toolPayload,
8356
+ action.cwd,
8357
+ action.repoRoot,
8358
+ params.options.trustedWorkspaceRoots
8359
+ )) {
8360
+ outsidePaths.add(resolved);
8361
+ }
8362
+ const command = extractShellCommandFromPayload(toolPayload);
8363
+ if (command) {
8364
+ for (const resolved of collectOutsideRepoPaths(
8365
+ command,
8366
+ action.cwd,
8367
+ action.repoRoot,
8368
+ params.options.trustedWorkspaceRoots
8369
+ )) {
8370
+ outsidePaths.add(resolved);
8371
+ }
8372
+ }
8373
+ }
8374
+ if (outsidePaths.size !== 1) {
8375
+ return void 0;
8376
+ }
8377
+ const targetPath = [...outsidePaths][0];
8378
+ if (!targetPath) {
8379
+ return void 0;
8380
+ }
8381
+ const candidateRoot = canonicalPath(path26.dirname(targetPath));
8382
+ const validation = validateTrustedWorkspaceRootCandidate({
8383
+ candidatePath: candidateRoot,
8384
+ repoRoot: action.repoRoot,
8385
+ controlPlaneDir: params.options.controlPlaneDir ?? void 0,
8386
+ protectedRoots: params.options.protectedArtifactRoots ?? [],
8387
+ requireExistingDirectory: true,
8388
+ requireNonGit: true
8389
+ });
8390
+ if (!validation.ok) {
8391
+ return void 0;
8392
+ }
8393
+ return { scope: "workspace-root", path: validation.normalizedPath };
8394
+ }
6125
8395
  async function consumeApprovedApproval(ctx, deps, kind, fingerprint) {
6126
8396
  const approved = await deps.loadApprovals(ctx, "approved-approvals.json");
6127
8397
  approved.state = compactApprovals(approved.state);
@@ -6139,7 +8409,7 @@ async function consumeApprovedApproval(ctx, deps, kind, fingerprint) {
6139
8409
  }
6140
8410
  approved.state.approvals[index] = {
6141
8411
  ...approval,
6142
- executionLeaseExpiresAt: new Date(Date.now() + APPROVAL_EXECUTION_LEASE_MS).toISOString()
8412
+ executionLeaseExpiresAt: new Date(Date.now() + getExecutionLeaseMs(ctx.config)).toISOString()
6143
8413
  };
6144
8414
  await deps.writeApprovals(approved.filePath, approved.state);
6145
8415
  return approval;
@@ -6205,7 +8475,7 @@ async function evaluateGatedAction(ctx, deps, params) {
6205
8475
  command: params.command,
6206
8476
  cwd: params.cwd,
6207
8477
  repoRoot: ctx.repoRoot,
6208
- stateDir: path20.join(ctx.layout.repoLocalStateDir(ctx.repoRoot), "transactional"),
8478
+ stateDir: path26.join(ctx.layout.repoLocalStateDir(ctx.repoRoot), "transactional"),
6209
8479
  timeoutMs: transactional.timeoutMs,
6210
8480
  predicted,
6211
8481
  diffContext: {
@@ -6233,20 +8503,39 @@ async function evaluateGatedAction(ctx, deps, params) {
6233
8503
  };
6234
8504
  }
6235
8505
  }
8506
+ const scrubOpts = scrubOptionsFromConfig(ctx.config);
8507
+ const scrubbedPayload = fingerprintReplayPayload(params.kind, params.payload, scrubOpts);
6236
8508
  return gateDecisionToVerdict(ctx, deps, params.kind, result, {
6237
8509
  predictedAssessment,
6238
8510
  observedAssessment: observedAssessment2,
6239
8511
  transactionalLayer,
6240
8512
  approvalInput: params.kind === "shell" ? {
6241
8513
  input: params.command ?? result.normalizedCommand ?? result.summary ?? "",
6242
- inputKind: "shell"
8514
+ inputKind: "shell",
8515
+ cwd: params.cwd
6243
8516
  } : {
6244
- input: params.command ?? result.normalizedCommand ?? result.summary ?? canonicalStringify(params.payload ?? {}),
6245
- inputKind: params.kind
6246
- }
8517
+ input: params.command ?? result.normalizedCommand ?? result.summary ?? canonicalStringify(scrubbedPayload ?? {}),
8518
+ inputKind: params.kind,
8519
+ cwd: params.cwd,
8520
+ toolName: params.toolName,
8521
+ payload: scrubbedPayload
8522
+ },
8523
+ replayAction: {
8524
+ kind: params.kind,
8525
+ cwd: params.cwd,
8526
+ toolName: params.toolName,
8527
+ command: params.command,
8528
+ payload: scrubbedPayload,
8529
+ fingerprint: result.fingerprint,
8530
+ repoRoot: ctx.repoRoot
8531
+ },
8532
+ classifierOptions,
8533
+ scopeHintPayload: params.payload
6247
8534
  });
6248
8535
  }
6249
8536
  async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {}) {
8537
+ const replayContext = buildAuditReplayContext(kind, result, auditExtras.replayAction);
8538
+ const actionSnapshot = buildAuditActionSnapshot(kind, result, auditExtras.replayAction);
6250
8539
  const gateBase = {
6251
8540
  event: gateAuditEventName(kind),
6252
8541
  kind,
@@ -6258,7 +8547,9 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
6258
8547
  mode: ctx.config.mode,
6259
8548
  schemaVersion: result.axes ? 2 : 1,
6260
8549
  ...result.axes ?? {},
6261
- ...auditExtras.transactionalLayer
8550
+ ...auditExtras.transactionalLayer,
8551
+ ...replayContext ? { replayContext } : {},
8552
+ ...actionSnapshot ? { actionSnapshot } : {}
6262
8553
  };
6263
8554
  if (result.reason === TRANSACTIONAL_ALREADY_APPLIED) {
6264
8555
  const userMessage = "Belay executed this command safely in an isolated git worktree. Observed-safe file changes are already applied; do not retry the same command.";
@@ -6279,6 +8570,36 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
6279
8570
  agent_message: agentMessage
6280
8571
  });
6281
8572
  }
8573
+ if (result.verdict === "deny_pending_approval" && ctx.config.mode === "enforce" && !TRANSACTIONAL_APPROVAL_BYPASS_REASONS.has(result.reason)) {
8574
+ const standingAllowPath = standingAllowFile(
8575
+ ctx.config,
8576
+ ctx.layout.repoLocalStateDir(ctx.repoRoot)
8577
+ );
8578
+ const standingState = await loadStandingAllow(standingAllowPath);
8579
+ const standingMatch = resolveStandingAllowMatch({
8580
+ kind,
8581
+ result,
8582
+ repoRoot: ctx.repoRoot,
8583
+ state: standingState
8584
+ });
8585
+ if (standingMatch) {
8586
+ await deps.appendAudit(ctx, {
8587
+ ...gateBase,
8588
+ verdict: "allow",
8589
+ reason: "standing_allow",
8590
+ standingAllowSource: standingMatch.source,
8591
+ ...standingMatch.catalogCommand ? { standingAllowCatalogCommand: standingMatch.catalogCommand } : {},
8592
+ wouldBlock: false,
8593
+ permission: "allow"
8594
+ });
8595
+ return classifyResultToGateVerdict({
8596
+ result: { ...result, verdict: "allow", reason: "standing_allow" },
8597
+ mode: ctx.config.mode,
8598
+ permission: "allow",
8599
+ wouldBlock: false
8600
+ });
8601
+ }
8602
+ }
6282
8603
  const brokerActive = isCapabilityBrokerDemotionActive(ctx.config);
6283
8604
  let approved = null;
6284
8605
  if (!TRANSACTIONAL_APPROVAL_BYPASS_REASONS.has(result.reason) && !shouldSkipBrokerApprovedOnce(brokerActive, result.reason)) {
@@ -6288,6 +8609,29 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
6288
8609
  (entry) => entry.kind === kind && entry.fingerprint === result.fingerprint && entry.repoRoot === ctx.repoRoot
6289
8610
  );
6290
8611
  if (!shouldSkipBrokerApprovedRecord(brokerActive, matchedApproval?.reason)) {
8612
+ if (matchedApproval && auditExtras.replayAction && !validateReplayEnvelope(matchedApproval, auditExtras.replayAction)) {
8613
+ await deps.appendAudit(ctx, {
8614
+ ...gateBase,
8615
+ verdict: "deny_pending_approval",
8616
+ reason: "approval_replay_mismatch",
8617
+ approvalId: matchedApproval.approvalId,
8618
+ wouldBlock: true,
8619
+ permission: "deny"
8620
+ });
8621
+ return classifyResultToGateVerdict({
8622
+ result: {
8623
+ ...result,
8624
+ verdict: "deny_pending_approval",
8625
+ reason: "approval_replay_mismatch"
8626
+ },
8627
+ mode: ctx.config.mode,
8628
+ permission: "deny",
8629
+ wouldBlock: true,
8630
+ approvalId: matchedApproval.approvalId,
8631
+ user_message: "Belay denied this action because it does not match the approved replay envelope. Re-approve the exact action or run belay explain.",
8632
+ agent_message: "Belay denied this action because cwd, tool, or payload changed after approval."
8633
+ });
8634
+ }
6291
8635
  approved = await consumeApprovedApproval(ctx, deps, kind, result.fingerprint);
6292
8636
  }
6293
8637
  }
@@ -6338,7 +8682,19 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
6338
8682
  wouldBlock: true
6339
8683
  });
6340
8684
  }
6341
- const approval = await ensurePendingApproval(ctx, deps, kind, result, auditExtras.approvalInput);
8685
+ const approval = await ensurePendingApproval(
8686
+ ctx,
8687
+ deps,
8688
+ kind,
8689
+ result,
8690
+ auditExtras.approvalInput,
8691
+ deriveWorkspaceRootScopeHint({
8692
+ result,
8693
+ replayAction: auditExtras.replayAction,
8694
+ payloadForScopeHint: auditExtras.scopeHintPayload,
8695
+ options: auditExtras.classifierOptions ?? {}
8696
+ })
8697
+ );
6342
8698
  let approvalToken;
6343
8699
  try {
6344
8700
  approvalToken = await issueApprovalToken(
@@ -6378,7 +8734,7 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
6378
8734
  permission: "deny",
6379
8735
  wouldBlock: true,
6380
8736
  approvalId: approval.approvalId,
6381
- user_message: `Belay blocked this high-risk action. Approval ID: ${approval.approvalId}. ${buildRetryInstruction(ctx.config.tokenPrefix, approval.approvalId)} For details, run belay explain or /belay why.`,
8737
+ user_message: `Belay blocked this high-risk action. Approval ID: ${approval.approvalId}. ${buildRetryInstructionForConfig(ctx.config, ctx.config.tokenPrefix, approval.approvalId)} For details, run belay explain or /belay why.`,
6382
8738
  agent_message: `Belay denied this action as ${result.reason}. Wait for approval, then retry the exact same action once.`
6383
8739
  });
6384
8740
  }
@@ -6387,6 +8743,7 @@ async function processApprovalPrompt(ctx, deps, prompt) {
6387
8743
  if (!approvalId) {
6388
8744
  return { continue: true };
6389
8745
  }
8746
+ const adapter = adapterIdFromContext(ctx);
6390
8747
  if (ctx.config.approvalSigning.required) {
6391
8748
  const message = `Signed approval token required for ${approvalId}. Editor prompt approval is disabled in this configuration. Use belay approve --approval-id ${approvalId} --token <signed-token>.`;
6392
8749
  await deps.appendAudit(ctx, {
@@ -6406,12 +8763,11 @@ async function processApprovalPrompt(ctx, deps, prompt) {
6406
8763
  approvalId,
6407
8764
  config: ctx.config,
6408
8765
  requireSignedToken: ctx.config.approvalSigning.required,
6409
- store: {
6410
- loadPending: () => deps.loadApprovals(ctx, "pending-approvals.json"),
6411
- loadApproved: () => deps.loadApprovals(ctx, "approved-approvals.json"),
6412
- writePending: (filePath, state) => deps.writeApprovals(filePath, state),
6413
- writeApproved: (filePath, state) => deps.writeApprovals(filePath, state)
6414
- }
8766
+ adapter,
8767
+ store: gateApprovalStoreFromDeps({
8768
+ loadApprovals: (fileName) => deps.loadApprovals(ctx, fileName),
8769
+ writeApprovals: (filePath, state) => deps.writeApprovals(filePath, state)
8770
+ })
6415
8771
  });
6416
8772
  await deps.appendAudit(ctx, {
6417
8773
  event: "approval",
@@ -6427,9 +8783,91 @@ async function processApprovalPrompt(ctx, deps, prompt) {
6427
8783
  user_message: recorded.message
6428
8784
  };
6429
8785
  }
8786
+ const replay = recorded.approval ? buildReplayHint(ctx.config, recorded.approval, adapter) : null;
8787
+ const shouldFallbackReplay = adapter === "cursor" && replay?.kind === "shell" && replay.autoReplay === true && Boolean(recorded.approval?.input) && (!process.env.VITEST || process.env.BELAY_TEST_APPROVAL_REPLAY === "1");
8788
+ if (shouldFallbackReplay && recorded.approval) {
8789
+ const approvalStore = gateApprovalStoreFromDeps({
8790
+ loadApprovals: (fileName) => deps.loadApprovals(ctx, fileName),
8791
+ writeApprovals: (filePath, state) => deps.writeApprovals(filePath, state)
8792
+ });
8793
+ let replayResult;
8794
+ try {
8795
+ replayResult = await replayShellCommand(
8796
+ recorded.approval.input ?? "",
8797
+ recorded.approval.cwd ?? ctx.repoRoot,
8798
+ getExecutionLeaseMs(ctx.config)
8799
+ );
8800
+ } catch (error) {
8801
+ const detail = error instanceof Error ? error.message : String(error);
8802
+ await deps.appendAudit(ctx, {
8803
+ event: "approval",
8804
+ kind: "approval",
8805
+ verdict: "allow",
8806
+ approvalId,
8807
+ reason: "approval_replay_error",
8808
+ summary: recorded.approval.input ?? recorded.approval.summary ?? prompt
8809
+ });
8810
+ return {
8811
+ continue: false,
8812
+ user_message: `Belay approval recorded for ${approvalId}, but one-step shell replay could not start (${detail}). Approval remains active for one hook retry.`
8813
+ };
8814
+ }
8815
+ const output = [replayResult.stdout, replayResult.stderr].filter(Boolean).join("\n").trim();
8816
+ if (replayResult.exitCode === 0) {
8817
+ try {
8818
+ await consumeApprovedAfterCliReplay({
8819
+ approvalId,
8820
+ store: approvalStore
8821
+ });
8822
+ } catch (error) {
8823
+ const detail = error instanceof Error ? error.message : String(error);
8824
+ await deps.appendAudit(ctx, {
8825
+ event: "approval",
8826
+ kind: "approval",
8827
+ verdict: "allow",
8828
+ approvalId,
8829
+ reason: "approval_replay_consume_error",
8830
+ summary: recorded.approval.input ?? recorded.approval.summary ?? prompt
8831
+ });
8832
+ return {
8833
+ continue: false,
8834
+ user_message: `Belay approval recorded for ${approvalId}, and one-step shell replay succeeded, but approval finalization failed (${detail}). Approval remains active for one hook retry.` + (output ? `
8835
+ ${output}` : "")
8836
+ };
8837
+ }
8838
+ await deps.appendAudit(ctx, {
8839
+ event: "approval",
8840
+ kind: "approval",
8841
+ verdict: "allow",
8842
+ approvalId,
8843
+ reason: "approval_replay_succeeded",
8844
+ summary: recorded.approval.input ?? recorded.approval.summary ?? prompt
8845
+ });
8846
+ return {
8847
+ continue: false,
8848
+ user_message: `Belay approval recorded for ${approvalId}. One-step shell replay succeeded; no manual retry required.` + (output ? `
8849
+ ${output}` : "")
8850
+ };
8851
+ }
8852
+ await deps.appendAudit(ctx, {
8853
+ event: "approval",
8854
+ kind: "approval",
8855
+ verdict: "allow",
8856
+ approvalId,
8857
+ reason: "approval_replay_failed",
8858
+ summary: recorded.approval.input ?? recorded.approval.summary ?? prompt
8859
+ });
8860
+ const timeoutNote = replayResult.timedOut ? " Replay timed out." : "";
8861
+ return {
8862
+ continue: false,
8863
+ user_message: `Belay approval recorded for ${approvalId}, but one-step shell replay failed (exit ${replayResult.exitCode}).${timeoutNote} Approval remains active for one hook retry.` + (output ? `
8864
+ ${output}` : "")
8865
+ };
8866
+ }
6430
8867
  return {
6431
8868
  continue: false,
6432
- user_message: recorded.message
8869
+ user_message: recorded.message,
8870
+ ...replay ? { replay } : {}
6433
8871
  };
6434
8872
  }
6435
8873
  function gateVerdictToClaudePreToolUseResponse(verdict2) {
@@ -6452,7 +8890,8 @@ function gateVerdictToClaudeUserPromptResponse(verdict2) {
6452
8890
  hookSpecificOutput: {
6453
8891
  hookEventName: "UserPromptSubmit",
6454
8892
  continue: false,
6455
- user_message: verdict2.user_message
8893
+ user_message: verdict2.user_message,
8894
+ ...verdict2.replay ? { replay: verdict2.replay } : {}
6456
8895
  }
6457
8896
  };
6458
8897
  }
@@ -6467,19 +8906,19 @@ async function appendObservedAudit(ctx, deps, eventName, payload) {
6467
8906
  }
6468
8907
 
6469
8908
  // src/adapters/shared/repo-root.ts
6470
- import { existsSync as existsSync7 } from "node:fs";
6471
- import path21 from "node:path";
8909
+ import { existsSync as existsSync11 } from "node:fs";
8910
+ import path27 from "node:path";
6472
8911
  function findRepoRoot(startPath, layout) {
6473
- let current = path21.resolve(startPath);
8912
+ let current = path27.resolve(startPath);
6474
8913
  while (true) {
6475
8914
  for (const marker of layout.repoRootMarkers) {
6476
- if (existsSync7(path21.join(current, marker))) {
8915
+ if (existsSync11(path27.join(current, marker))) {
6477
8916
  return current;
6478
8917
  }
6479
8918
  }
6480
- const parent = path21.dirname(current);
8919
+ const parent = path27.dirname(current);
6481
8920
  if (parent === current) {
6482
- return path21.resolve(startPath);
8921
+ return path27.resolve(startPath);
6483
8922
  }
6484
8923
  current = parent;
6485
8924
  }