@guilz-dev/belay 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/cursor/runtime-entry.js +1 -0
- package/dist/adapters/shared/gate-runtime.d.ts +11 -14
- package/dist/adapters/shared/gate-runtime.js +277 -19
- package/dist/bundle/claude-runtime.mjs +3138 -699
- package/dist/bundle/codex-runtime.mjs +3162 -711
- package/dist/bundle/cursor-runtime.mjs +3140 -701
- package/dist/cli.js +137 -5
- package/dist/commands/approve.d.ts +1 -0
- package/dist/commands/approve.js +45 -26
- package/dist/commands/config.js +135 -52
- package/dist/commands/doctor.js +11 -1
- package/dist/commands/explain.js +6 -1
- package/dist/commands/harvest.d.ts +29 -0
- package/dist/commands/harvest.js +72 -0
- package/dist/commands/judge.d.ts +11 -1
- package/dist/commands/judge.js +21 -2
- package/dist/commands/metrics.js +34 -2
- package/dist/commands/quality.d.ts +38 -0
- package/dist/commands/quality.js +105 -0
- package/dist/commands/simulate.d.ts +1 -0
- package/dist/commands/simulate.js +7 -2
- package/dist/commands/standing-allow.d.ts +10 -0
- package/dist/commands/standing-allow.js +26 -0
- package/dist/commands/tui.d.ts +48 -0
- package/dist/commands/tui.js +150 -0
- package/dist/config-io.js +10 -5
- package/dist/core/approval-replay-cli.d.ts +7 -0
- package/dist/core/approval-replay-cli.js +36 -0
- package/dist/core/approval-replay.d.ts +45 -0
- package/dist/core/approval-replay.js +141 -0
- package/dist/core/approval-service.d.ts +15 -0
- package/dist/core/approval-service.js +47 -2
- package/dist/core/approval.d.ts +24 -3
- package/dist/core/approval.js +47 -4
- package/dist/core/audit-analysis.d.ts +7 -1
- package/dist/core/audit-analysis.js +111 -0
- package/dist/core/audit-metrics.d.ts +7 -0
- package/dist/core/audit-metrics.js +24 -4
- package/dist/core/audit-query.d.ts +1 -0
- package/dist/core/audit-query.js +3 -0
- package/dist/core/audit-replay-context.d.ts +35 -0
- package/dist/core/audit-replay-context.js +88 -0
- package/dist/core/audit-types.d.ts +24 -1
- package/dist/core/audit-types.js +1 -1
- package/dist/core/capability/index.d.ts +2 -1
- package/dist/core/capability/index.js +1 -0
- package/dist/core/capability/paths.d.ts +2 -2
- package/dist/core/capability/paths.js +9 -9
- package/dist/core/capability/trusted-workspace-roots.d.ts +25 -0
- package/dist/core/capability/trusted-workspace-roots.js +149 -0
- package/dist/core/capability/types.d.ts +11 -1
- package/dist/core/capability-approval.d.ts +2 -1
- package/dist/core/capability-approval.js +100 -2
- package/dist/core/classify-subagent.js +2 -20
- package/dist/core/classify-tool.js +32 -3
- package/dist/core/config.d.ts +18 -0
- package/dist/core/config.js +49 -0
- package/dist/core/gate-engine.js +6 -6
- package/dist/core/harvest.d.ts +53 -0
- package/dist/core/harvest.js +276 -0
- package/dist/core/index.d.ts +9 -3
- package/dist/core/index.js +6 -2
- package/dist/core/judge-doctor.d.ts +3 -0
- package/dist/core/judge-doctor.js +54 -0
- package/dist/core/path-utils.d.ts +11 -0
- package/dist/core/path-utils.js +56 -6
- package/dist/core/reclassify.d.ts +3 -0
- package/dist/core/reclassify.js +44 -14
- package/dist/core/replay-scrub.d.ts +9 -0
- package/dist/core/replay-scrub.js +43 -0
- package/dist/core/shell-tokenizer.d.ts +2 -0
- package/dist/core/shell-tokenizer.js +82 -23
- package/dist/core/standing-allow.d.ts +50 -0
- package/dist/core/standing-allow.js +175 -0
- package/dist/core/types.d.ts +15 -0
- package/dist/core/verdict/adapter.js +10 -1
- package/dist/core/verdict/containment.d.ts +4 -3
- package/dist/core/verdict/containment.js +17 -12
- package/dist/core/verdict/judge-audit.d.ts +1 -0
- package/dist/core/verdict/judge-audit.js +32 -1
- package/dist/core/verdict/judge-baseline.d.ts +20 -0
- package/dist/core/verdict/judge-baseline.js +48 -0
- package/dist/core/verdict/judge-broker-service.d.ts +42 -0
- package/dist/core/verdict/judge-broker-service.js +279 -0
- package/dist/core/verdict/judge-cli-fingerprint.d.ts +5 -0
- package/dist/core/verdict/judge-cli-fingerprint.js +61 -0
- package/dist/core/verdict/judge-cli.d.ts +23 -0
- package/dist/core/verdict/judge-cli.js +137 -16
- package/dist/core/verdict/judge-factory.js +13 -4
- package/dist/core/verdict/judge-provider-matrix.d.ts +20 -0
- package/dist/core/verdict/judge-provider-matrix.js +66 -0
- package/dist/core/verdict/judge-runtime-config.d.ts +42 -0
- package/dist/core/verdict/judge-runtime-config.js +92 -0
- package/dist/core/verdict/judge-session-broker.d.ts +48 -0
- package/dist/core/verdict/judge-session-broker.js +195 -0
- package/dist/core/verdict/judge-session-guard.d.ts +27 -0
- package/dist/core/verdict/judge-session-guard.js +91 -0
- package/dist/core/verdict/judge-session-kill-switch.d.ts +11 -0
- package/dist/core/verdict/judge-session-kill-switch.js +43 -0
- package/dist/core/verdict/judge-session-mutex.d.ts +9 -0
- package/dist/core/verdict/judge-session-mutex.js +23 -0
- package/dist/core/verdict/judge-shadow.d.ts +24 -0
- package/dist/core/verdict/judge-shadow.js +116 -0
- package/dist/core/verdict/judge-transport.d.ts +40 -0
- package/dist/core/verdict/judge-transport.js +258 -0
- package/dist/core/verdict/judge.d.ts +14 -1
- package/dist/core/verdict/judge.js +6 -5
- package/dist/core/verdict/parser.js +6 -1
- package/dist/core/verdict/types.d.ts +13 -0
- package/dist/core/verdict/verdict.js +7 -4
- package/dist/corpus/adversarial-probe.d.ts +86 -0
- package/dist/corpus/adversarial-probe.js +220 -0
- package/dist/corpus/evaluate.d.ts +45 -12
- package/dist/corpus/evaluate.js +62 -5
- package/dist/corpus/gates.d.ts +35 -0
- package/dist/corpus/gates.js +81 -0
- package/dist/corpus/judge-accuracy.d.ts +8 -0
- package/dist/corpus/judge-accuracy.js +39 -0
- package/dist/corpus/must-allow-commands.d.ts +5 -0
- package/dist/corpus/must-allow-commands.js +13 -0
- package/dist/corpus/mutators.d.ts +28 -0
- package/dist/corpus/mutators.js +151 -0
- package/dist/corpus/ratchet.d.ts +42 -0
- package/dist/corpus/ratchet.js +116 -0
- package/dist/corpus/runtime-match.d.ts +19 -0
- package/dist/corpus/runtime-match.js +45 -0
- package/dist/corpus/standing-allow-catalog.generated.d.ts +13 -0
- package/dist/corpus/standing-allow-catalog.generated.js +79 -0
- package/dist/corpus/types.d.ts +47 -0
- package/dist/corpus/types.js +157 -0
- package/dist/installer.js +4 -1
- package/dist/judge-broker-daemon.d.ts +1 -0
- package/dist/judge-broker-daemon.js +123 -0
- package/dist/services/sandbox-service.d.ts +1 -0
- package/dist/services/sandbox-service.js +2 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -3
- package/skills/belay/SKILL.md +7 -1
- package/skills/belay/belay-approve.md +17 -0
|
@@ -154,6 +154,115 @@ function isCloudProviderId(providerId) {
|
|
|
154
154
|
return JUDGE_CATALOG[normalized].isCloud;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
// src/core/verdict/judge-runtime-config.ts
|
|
158
|
+
var DEFAULT_JUDGE_SESSION_CONFIG = {
|
|
159
|
+
enabled: false,
|
|
160
|
+
maxTurns: 32,
|
|
161
|
+
maxAgeMs: 30 * 60 * 1e3,
|
|
162
|
+
maxIdleMs: 5 * 60 * 1e3,
|
|
163
|
+
maxPromptBytes: 64 * 1024,
|
|
164
|
+
providerAllowlist: ["cursor"],
|
|
165
|
+
connectTimeoutMs: 5e3,
|
|
166
|
+
evalTimeoutMs: null,
|
|
167
|
+
parseTimeoutMs: 2e3,
|
|
168
|
+
maxSessionsPerProvider: 1
|
|
169
|
+
};
|
|
170
|
+
var DEFAULT_JUDGE_SHADOW_CONFIG = {
|
|
171
|
+
enabled: false,
|
|
172
|
+
sampleRate: 0.01,
|
|
173
|
+
sampleRateMax: 0.05,
|
|
174
|
+
dailyRequestCap: 500,
|
|
175
|
+
mismatchRateThreshold: 0.02,
|
|
176
|
+
providerAllowlist: ["cursor"],
|
|
177
|
+
windowSize: 100
|
|
178
|
+
};
|
|
179
|
+
var DEFAULT_JUDGE_RUNTIME_CONFIG = {
|
|
180
|
+
session: { ...DEFAULT_JUDGE_SESSION_CONFIG },
|
|
181
|
+
shadow: { ...DEFAULT_JUDGE_SHADOW_CONFIG }
|
|
182
|
+
};
|
|
183
|
+
var CLI_PROVIDER_IDS = ["codex", "cursor", "claude"];
|
|
184
|
+
function normalizeProviderAllowlist(value, fallback) {
|
|
185
|
+
if (!Array.isArray(value)) {
|
|
186
|
+
return [...fallback];
|
|
187
|
+
}
|
|
188
|
+
const allowed = value.map((entry) => String(entry).trim()).filter(
|
|
189
|
+
(entry) => CLI_PROVIDER_IDS.includes(entry)
|
|
190
|
+
);
|
|
191
|
+
return allowed.length > 0 ? [...new Set(allowed)] : [...fallback];
|
|
192
|
+
}
|
|
193
|
+
function normalizePositiveInt(value, fallback) {
|
|
194
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
195
|
+
}
|
|
196
|
+
function normalizeRate(value, fallback, max) {
|
|
197
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
198
|
+
return fallback;
|
|
199
|
+
}
|
|
200
|
+
return Math.min(value, max);
|
|
201
|
+
}
|
|
202
|
+
function normalizeJudgeRuntimeConfig(runtime) {
|
|
203
|
+
const session = runtime?.session ?? {};
|
|
204
|
+
const shadow = runtime?.shadow ?? {};
|
|
205
|
+
return {
|
|
206
|
+
session: normalizeJudgeSessionConfig(session),
|
|
207
|
+
shadow: {
|
|
208
|
+
enabled: shadow.enabled === true,
|
|
209
|
+
sampleRate: normalizeRate(shadow.sampleRate, DEFAULT_JUDGE_SHADOW_CONFIG.sampleRate, 1),
|
|
210
|
+
sampleRateMax: normalizeRate(
|
|
211
|
+
shadow.sampleRateMax,
|
|
212
|
+
DEFAULT_JUDGE_SHADOW_CONFIG.sampleRateMax,
|
|
213
|
+
1
|
|
214
|
+
),
|
|
215
|
+
dailyRequestCap: normalizePositiveInt(
|
|
216
|
+
shadow.dailyRequestCap,
|
|
217
|
+
DEFAULT_JUDGE_SHADOW_CONFIG.dailyRequestCap
|
|
218
|
+
),
|
|
219
|
+
mismatchRateThreshold: normalizeRate(
|
|
220
|
+
shadow.mismatchRateThreshold,
|
|
221
|
+
DEFAULT_JUDGE_SHADOW_CONFIG.mismatchRateThreshold,
|
|
222
|
+
1
|
|
223
|
+
),
|
|
224
|
+
providerAllowlist: normalizeProviderAllowlist(
|
|
225
|
+
shadow.providerAllowlist,
|
|
226
|
+
DEFAULT_JUDGE_SHADOW_CONFIG.providerAllowlist
|
|
227
|
+
),
|
|
228
|
+
windowSize: normalizePositiveInt(shadow.windowSize, DEFAULT_JUDGE_SHADOW_CONFIG.windowSize)
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function normalizeJudgeSessionConfig(session) {
|
|
233
|
+
const input = session ?? {};
|
|
234
|
+
return {
|
|
235
|
+
enabled: input.enabled === true,
|
|
236
|
+
maxTurns: normalizePositiveInt(input.maxTurns, DEFAULT_JUDGE_SESSION_CONFIG.maxTurns),
|
|
237
|
+
maxAgeMs: normalizePositiveInt(input.maxAgeMs, DEFAULT_JUDGE_SESSION_CONFIG.maxAgeMs),
|
|
238
|
+
maxIdleMs: normalizePositiveInt(input.maxIdleMs, DEFAULT_JUDGE_SESSION_CONFIG.maxIdleMs),
|
|
239
|
+
maxPromptBytes: normalizePositiveInt(
|
|
240
|
+
input.maxPromptBytes,
|
|
241
|
+
DEFAULT_JUDGE_SESSION_CONFIG.maxPromptBytes
|
|
242
|
+
),
|
|
243
|
+
providerAllowlist: normalizeProviderAllowlist(
|
|
244
|
+
input.providerAllowlist,
|
|
245
|
+
DEFAULT_JUDGE_SESSION_CONFIG.providerAllowlist
|
|
246
|
+
),
|
|
247
|
+
connectTimeoutMs: normalizePositiveInt(
|
|
248
|
+
input.connectTimeoutMs,
|
|
249
|
+
DEFAULT_JUDGE_SESSION_CONFIG.connectTimeoutMs
|
|
250
|
+
),
|
|
251
|
+
evalTimeoutMs: typeof input.evalTimeoutMs === "number" && input.evalTimeoutMs > 0 ? Math.floor(input.evalTimeoutMs) : null,
|
|
252
|
+
parseTimeoutMs: normalizePositiveInt(
|
|
253
|
+
input.parseTimeoutMs,
|
|
254
|
+
DEFAULT_JUDGE_SESSION_CONFIG.parseTimeoutMs
|
|
255
|
+
),
|
|
256
|
+
maxSessionsPerProvider: normalizePositiveInt(
|
|
257
|
+
input.maxSessionsPerProvider,
|
|
258
|
+
DEFAULT_JUDGE_SESSION_CONFIG.maxSessionsPerProvider
|
|
259
|
+
)
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function resolveSessionEvalTimeoutMs(sessionConfig, judgeTimeoutMs) {
|
|
263
|
+
return sessionConfig.evalTimeoutMs ?? judgeTimeoutMs;
|
|
264
|
+
}
|
|
265
|
+
|
|
157
266
|
// src/core/audit-summary.ts
|
|
158
267
|
var DEFAULT_SILENT_PASS_THRESHOLD = 0.5;
|
|
159
268
|
|
|
@@ -246,6 +355,15 @@ var DEFAULT_NOTIFICATIONS_V3 = {};
|
|
|
246
355
|
var DEFAULT_APPROVAL_SIGNING_V3 = {
|
|
247
356
|
required: false
|
|
248
357
|
};
|
|
358
|
+
var DEFAULT_APPROVAL_CONFIG = {
|
|
359
|
+
flow: "one_step",
|
|
360
|
+
autoReplayScopes: {
|
|
361
|
+
shell: true,
|
|
362
|
+
tool: false,
|
|
363
|
+
subagent: false
|
|
364
|
+
},
|
|
365
|
+
executionLeaseMs: 6e4
|
|
366
|
+
};
|
|
249
367
|
var DEFAULT_EGRESS_V3 = {
|
|
250
368
|
enabled: false,
|
|
251
369
|
listenHost: "127.0.0.1",
|
|
@@ -299,6 +417,7 @@ var DEFAULT_CONFIG_V4 = {
|
|
|
299
417
|
controlPlane: { ...DEFAULT_CONTROL_PLANE_V3 },
|
|
300
418
|
notifications: { ...DEFAULT_NOTIFICATIONS_V3 },
|
|
301
419
|
approvalSigning: { ...DEFAULT_APPROVAL_SIGNING_V3 },
|
|
420
|
+
approval: { ...DEFAULT_APPROVAL_CONFIG },
|
|
302
421
|
egress: { ...DEFAULT_EGRESS_V3 },
|
|
303
422
|
sandbox: { ...DEFAULT_SANDBOX_V3 },
|
|
304
423
|
audit: { ...DEFAULT_CONFIG_V2.audit },
|
|
@@ -338,6 +457,7 @@ function migrateV2ToV3(v2, rawOverrides) {
|
|
|
338
457
|
controlPlane: { ...LEGACY_CONTROL_PLANE_V3 },
|
|
339
458
|
notifications: { ...DEFAULT_NOTIFICATIONS_V3 },
|
|
340
459
|
approvalSigning: { ...DEFAULT_APPROVAL_SIGNING_V3 },
|
|
460
|
+
approval: { ...DEFAULT_APPROVAL_CONFIG },
|
|
341
461
|
egress: { ...DEFAULT_EGRESS_V3 },
|
|
342
462
|
sandbox: { ...DEFAULT_SANDBOX_V3 },
|
|
343
463
|
audit: v2.audit,
|
|
@@ -406,7 +526,8 @@ function normalizeJudgeConfig(judge) {
|
|
|
406
526
|
endpoint: endpoint2,
|
|
407
527
|
keepAlive: provider === "ollama" && typeof judge.keepAlive === "string" && judge.keepAlive.trim() ? judge.keepAlive.trim() : provider === "ollama" ? DEFAULT_JUDGE_LOCAL_OLLAMA.keepAlive : null,
|
|
408
528
|
...judge.cloudConsent ? { cloudConsent: judge.cloudConsent } : {},
|
|
409
|
-
...judge.credential ? { credential: judge.credential } : {}
|
|
529
|
+
...judge.credential ? { credential: judge.credential } : {},
|
|
530
|
+
runtime: normalizeJudgeRuntimeConfig(judge.runtime)
|
|
410
531
|
};
|
|
411
532
|
}
|
|
412
533
|
let providerId = rawProviderId && normalizeLegacyProviderId(rawProviderId) ? normalizeLegacyProviderId(rawProviderId) : inferProviderIdFromConfig({ ...judge, provider });
|
|
@@ -451,6 +572,7 @@ function normalizeJudgeConfig(judge) {
|
|
|
451
572
|
...judge.credential.ref ? { ref: judge.credential.ref } : {}
|
|
452
573
|
};
|
|
453
574
|
}
|
|
575
|
+
normalized.runtime = normalizeJudgeRuntimeConfig(judge.runtime);
|
|
454
576
|
return normalized;
|
|
455
577
|
}
|
|
456
578
|
function rejectTeamLayerJudgeSecrets(judge, source) {
|
|
@@ -499,6 +621,14 @@ function mergeV3FromRaw(base, raw) {
|
|
|
499
621
|
...base.approvalSigning,
|
|
500
622
|
...raw.approvalSigning ?? {}
|
|
501
623
|
},
|
|
624
|
+
approval: {
|
|
625
|
+
...base.approval,
|
|
626
|
+
...raw.approval ?? {},
|
|
627
|
+
autoReplayScopes: {
|
|
628
|
+
...base.approval.autoReplayScopes,
|
|
629
|
+
...raw.approval?.autoReplayScopes ?? {}
|
|
630
|
+
}
|
|
631
|
+
},
|
|
502
632
|
egress: {
|
|
503
633
|
...base.egress,
|
|
504
634
|
...raw.egress ?? {}
|
|
@@ -550,6 +680,14 @@ function normalizeV3Raw(raw) {
|
|
|
550
680
|
approvalSigning: {
|
|
551
681
|
required: raw.approvalSigning?.required === true
|
|
552
682
|
},
|
|
683
|
+
approval: {
|
|
684
|
+
...DEFAULT_APPROVAL_CONFIG,
|
|
685
|
+
...raw.approval ?? {},
|
|
686
|
+
autoReplayScopes: {
|
|
687
|
+
...DEFAULT_APPROVAL_CONFIG.autoReplayScopes,
|
|
688
|
+
...raw.approval?.autoReplayScopes ?? {}
|
|
689
|
+
}
|
|
690
|
+
},
|
|
553
691
|
egress: {
|
|
554
692
|
...DEFAULT_EGRESS_V3,
|
|
555
693
|
...raw.egress ?? {}
|
|
@@ -719,6 +857,15 @@ function normalizeConfig(config) {
|
|
|
719
857
|
approvalSigning: {
|
|
720
858
|
required: v4.approvalSigning?.required === true
|
|
721
859
|
},
|
|
860
|
+
approval: {
|
|
861
|
+
flow: v4.approval?.flow === "two_step" ? "two_step" : DEFAULT_APPROVAL_CONFIG.flow,
|
|
862
|
+
autoReplayScopes: {
|
|
863
|
+
shell: v4.approval?.autoReplayScopes?.shell !== false,
|
|
864
|
+
tool: v4.approval?.autoReplayScopes?.tool === true,
|
|
865
|
+
subagent: v4.approval?.autoReplayScopes?.subagent === true
|
|
866
|
+
},
|
|
867
|
+
executionLeaseMs: typeof v4.approval?.executionLeaseMs === "number" && v4.approval.executionLeaseMs > 0 ? v4.approval.executionLeaseMs : DEFAULT_APPROVAL_CONFIG.executionLeaseMs
|
|
868
|
+
},
|
|
722
869
|
egress: {
|
|
723
870
|
enabled: v4.egress?.enabled === true,
|
|
724
871
|
listenHost: normalizeEgressListenHost(
|
|
@@ -786,6 +933,14 @@ function mergeConfig(existing, defaults = DEFAULT_CONFIG_V4) {
|
|
|
786
933
|
...defaults.approvalSigning,
|
|
787
934
|
...migrated.approvalSigning
|
|
788
935
|
},
|
|
936
|
+
approval: {
|
|
937
|
+
...defaults.approval,
|
|
938
|
+
...migrated.approval,
|
|
939
|
+
autoReplayScopes: {
|
|
940
|
+
...defaults.approval.autoReplayScopes,
|
|
941
|
+
...migrated.approval.autoReplayScopes
|
|
942
|
+
}
|
|
943
|
+
},
|
|
789
944
|
egress: {
|
|
790
945
|
...defaults.egress,
|
|
791
946
|
...migrated.egress
|
|
@@ -903,12 +1058,216 @@ var cursorLayout = {
|
|
|
903
1058
|
|
|
904
1059
|
// src/adapters/shared/gate-runtime.ts
|
|
905
1060
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
906
|
-
import { existsSync as
|
|
907
|
-
import { mkdir as
|
|
908
|
-
import
|
|
1061
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
1062
|
+
import { mkdir as mkdir6, readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
|
|
1063
|
+
import path26 from "node:path";
|
|
1064
|
+
|
|
1065
|
+
// src/core/approval-replay.ts
|
|
1066
|
+
import path5 from "node:path";
|
|
1067
|
+
|
|
1068
|
+
// src/core/fingerprint.ts
|
|
1069
|
+
import { createHash } from "node:crypto";
|
|
1070
|
+
function canonicalStringify(value) {
|
|
1071
|
+
if (value === null || typeof value !== "object") {
|
|
1072
|
+
return JSON.stringify(value);
|
|
1073
|
+
}
|
|
1074
|
+
if (Array.isArray(value)) {
|
|
1075
|
+
return `[${value.map((item) => canonicalStringify(item)).join(",")}]`;
|
|
1076
|
+
}
|
|
1077
|
+
const entries = Object.entries(value).sort(
|
|
1078
|
+
([left], [right]) => left.localeCompare(right)
|
|
1079
|
+
);
|
|
1080
|
+
return `{${entries.map(([key, child]) => `${JSON.stringify(key)}:${canonicalStringify(child)}`).join(",")}}`;
|
|
1081
|
+
}
|
|
1082
|
+
function hashValue(value) {
|
|
1083
|
+
return createHash("sha256").update(value).digest("hex");
|
|
1084
|
+
}
|
|
1085
|
+
function subagentFingerprint(kind, scrubbed, repoRoot) {
|
|
1086
|
+
return hashValue(`subagent:${kind}:${canonicalStringify(scrubbed)}:${repoRoot}`);
|
|
1087
|
+
}
|
|
1088
|
+
function toolFingerprint(toolName, scrubbed, repoRoot) {
|
|
1089
|
+
return hashValue(`tool:${toolName}:${canonicalStringify(scrubbed)}:${repoRoot}`);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// src/core/approval-replay-cli.ts
|
|
1093
|
+
import { spawn } from "node:child_process";
|
|
1094
|
+
import path4 from "node:path";
|
|
1095
|
+
var DEFAULT_CLI_REPLAY_TIMEOUT_MS = 3e5;
|
|
1096
|
+
async function replayShellCommand(command, cwd, timeoutMs = DEFAULT_CLI_REPLAY_TIMEOUT_MS) {
|
|
1097
|
+
const resolvedCwd = path4.resolve(cwd);
|
|
1098
|
+
return new Promise((resolve, reject) => {
|
|
1099
|
+
const child = spawn(command, {
|
|
1100
|
+
cwd: resolvedCwd,
|
|
1101
|
+
shell: true,
|
|
1102
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1103
|
+
env: { ...process.env }
|
|
1104
|
+
});
|
|
1105
|
+
const stdout = [];
|
|
1106
|
+
const stderr = [];
|
|
1107
|
+
let timedOut = false;
|
|
1108
|
+
const timer = setTimeout(() => {
|
|
1109
|
+
timedOut = true;
|
|
1110
|
+
child.kill("SIGTERM");
|
|
1111
|
+
}, timeoutMs);
|
|
1112
|
+
child.stdout?.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
|
|
1113
|
+
child.stderr?.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
|
|
1114
|
+
child.on("error", (error) => {
|
|
1115
|
+
clearTimeout(timer);
|
|
1116
|
+
reject(error);
|
|
1117
|
+
});
|
|
1118
|
+
child.on("close", (code) => {
|
|
1119
|
+
clearTimeout(timer);
|
|
1120
|
+
resolve({
|
|
1121
|
+
exitCode: timedOut ? 124 : code ?? 1,
|
|
1122
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
1123
|
+
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
1124
|
+
...timedOut ? { timedOut: true } : {}
|
|
1125
|
+
});
|
|
1126
|
+
});
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// src/core/approval-replay.ts
|
|
1131
|
+
var MAX_PAYLOAD_JSON_BYTES = 16384;
|
|
1132
|
+
function replayPayloadHash(kind, payload, repoRoot) {
|
|
1133
|
+
if (!payload || Object.keys(payload).length === 0) {
|
|
1134
|
+
return void 0;
|
|
1135
|
+
}
|
|
1136
|
+
const json = canonicalStringify(payload);
|
|
1137
|
+
return hashValue(`payload:${kind}:${json}:${repoRoot}`);
|
|
1138
|
+
}
|
|
1139
|
+
var DEFAULT_REPLAY_CAPABILITIES = {
|
|
1140
|
+
shell: true,
|
|
1141
|
+
tool: false,
|
|
1142
|
+
subagent: false
|
|
1143
|
+
};
|
|
1144
|
+
var ADAPTER_REPLAY_CAPABLE = {
|
|
1145
|
+
cursor: { ...DEFAULT_REPLAY_CAPABILITIES },
|
|
1146
|
+
claude: { ...DEFAULT_REPLAY_CAPABILITIES },
|
|
1147
|
+
codex: { ...DEFAULT_REPLAY_CAPABILITIES }
|
|
1148
|
+
};
|
|
1149
|
+
function getExecutionLeaseMs(config) {
|
|
1150
|
+
const ms = config.approval?.executionLeaseMs;
|
|
1151
|
+
return typeof ms === "number" && ms > 0 ? ms : 6e4;
|
|
1152
|
+
}
|
|
1153
|
+
function approvalFlow(config) {
|
|
1154
|
+
return config.approval?.flow === "two_step" ? "two_step" : "one_step";
|
|
1155
|
+
}
|
|
1156
|
+
function buildReplayEnvelopeFields(input) {
|
|
1157
|
+
const fields = {};
|
|
1158
|
+
const cmd = input.command ?? input.input ?? "";
|
|
1159
|
+
if (cmd) {
|
|
1160
|
+
fields.input = cmd;
|
|
1161
|
+
fields.inputKind = input.inputKind ?? input.kind;
|
|
1162
|
+
}
|
|
1163
|
+
if (input.cwd) {
|
|
1164
|
+
fields.cwd = path5.resolve(input.cwd);
|
|
1165
|
+
}
|
|
1166
|
+
if (input.toolName) {
|
|
1167
|
+
fields.toolName = input.toolName;
|
|
1168
|
+
}
|
|
1169
|
+
if (input.payload && Object.keys(input.payload).length > 0) {
|
|
1170
|
+
const json = canonicalStringify(input.payload);
|
|
1171
|
+
if (Buffer.byteLength(json, "utf8") <= MAX_PAYLOAD_JSON_BYTES) {
|
|
1172
|
+
fields.payloadJson = json;
|
|
1173
|
+
}
|
|
1174
|
+
fields.payloadHash = replayPayloadHash(input.kind, input.payload, input.repoRoot);
|
|
1175
|
+
}
|
|
1176
|
+
return fields;
|
|
1177
|
+
}
|
|
1178
|
+
function hasReplayEnvelope(approval) {
|
|
1179
|
+
return Boolean(approval.cwd || approval.toolName || approval.payloadHash);
|
|
1180
|
+
}
|
|
1181
|
+
function validateReplayEnvelope(approval, action) {
|
|
1182
|
+
if (!hasReplayEnvelope(approval)) {
|
|
1183
|
+
return true;
|
|
1184
|
+
}
|
|
1185
|
+
if (approval.kind !== action.kind) {
|
|
1186
|
+
return false;
|
|
1187
|
+
}
|
|
1188
|
+
if (approval.fingerprint !== action.fingerprint) {
|
|
1189
|
+
return false;
|
|
1190
|
+
}
|
|
1191
|
+
if (approval.repoRoot !== action.repoRoot) {
|
|
1192
|
+
return false;
|
|
1193
|
+
}
|
|
1194
|
+
if (approval.cwd && path5.resolve(approval.cwd) !== path5.resolve(action.cwd ?? "")) {
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
if (approval.toolName && approval.toolName !== action.toolName) {
|
|
1198
|
+
return false;
|
|
1199
|
+
}
|
|
1200
|
+
if (approval.payloadHash) {
|
|
1201
|
+
const hash = replayPayloadHash(action.kind, action.payload, action.repoRoot);
|
|
1202
|
+
if (!hash || approval.payloadHash !== hash) {
|
|
1203
|
+
return false;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return true;
|
|
1207
|
+
}
|
|
1208
|
+
function canAutoReplay(config, kind, adapter) {
|
|
1209
|
+
if (config.approvalSigning?.required) {
|
|
1210
|
+
return false;
|
|
1211
|
+
}
|
|
1212
|
+
if (approvalFlow(config) !== "one_step") {
|
|
1213
|
+
return false;
|
|
1214
|
+
}
|
|
1215
|
+
if (kind !== "shell" && kind !== "tool" && kind !== "subagent") {
|
|
1216
|
+
return false;
|
|
1217
|
+
}
|
|
1218
|
+
const scopes = config.approval?.autoReplayScopes;
|
|
1219
|
+
if (kind === "shell" && scopes?.shell === false) {
|
|
1220
|
+
return false;
|
|
1221
|
+
}
|
|
1222
|
+
if (kind === "tool" && scopes?.tool !== true) {
|
|
1223
|
+
return false;
|
|
1224
|
+
}
|
|
1225
|
+
if (kind === "subagent" && scopes?.subagent !== true) {
|
|
1226
|
+
return false;
|
|
1227
|
+
}
|
|
1228
|
+
if (adapter) {
|
|
1229
|
+
return ADAPTER_REPLAY_CAPABLE[adapter][kind] === true;
|
|
1230
|
+
}
|
|
1231
|
+
return kind === "shell" && scopes?.shell !== false;
|
|
1232
|
+
}
|
|
1233
|
+
function buildRetryInstructionForConfig(config, tokenPrefix, approvalId) {
|
|
1234
|
+
if (approvalFlow(config) === "one_step") {
|
|
1235
|
+
return `To allow this action once, send ${tokenPrefix} ${approvalId}. After approval, retry the same action unchanged.`;
|
|
1236
|
+
}
|
|
1237
|
+
return `To allow the next matching action once, send ${tokenPrefix} ${approvalId} and then retry the original action unchanged.`;
|
|
1238
|
+
}
|
|
1239
|
+
function buildApprovalRecordedMessage(config, approval, adapter) {
|
|
1240
|
+
if (!canAutoReplay(config, approval.kind, adapter)) {
|
|
1241
|
+
return `Belay approval recorded for ${approval.approvalId}. Retry the original action once before it expires.`;
|
|
1242
|
+
}
|
|
1243
|
+
if (approval.kind === "shell" && approval.input) {
|
|
1244
|
+
return `Belay approval recorded for ${approval.approvalId}. Retry this shell command unchanged: ${approval.input}`;
|
|
1245
|
+
}
|
|
1246
|
+
return `Belay approval recorded for ${approval.approvalId}. Retry the original action once before it expires.`;
|
|
1247
|
+
}
|
|
1248
|
+
function buildReplayHint(config, approval, adapter) {
|
|
1249
|
+
if (approvalFlow(config) !== "one_step") {
|
|
1250
|
+
return null;
|
|
1251
|
+
}
|
|
1252
|
+
const input = approval.input ?? approval.summary;
|
|
1253
|
+
if (!input) {
|
|
1254
|
+
return null;
|
|
1255
|
+
}
|
|
1256
|
+
const autoReplay = canAutoReplay(config, approval.kind, adapter);
|
|
1257
|
+
return {
|
|
1258
|
+
kind: approval.kind,
|
|
1259
|
+
input,
|
|
1260
|
+
cwd: approval.cwd,
|
|
1261
|
+
toolName: approval.toolName,
|
|
1262
|
+
fingerprint: approval.fingerprint,
|
|
1263
|
+
approvalId: approval.approvalId,
|
|
1264
|
+
autoReplay,
|
|
1265
|
+
fallbackToTwoStep: !autoReplay
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
909
1268
|
|
|
910
1269
|
// src/core/approval.ts
|
|
911
|
-
var APPROVAL_EXECUTION_LEASE_MS =
|
|
1270
|
+
var APPROVAL_EXECUTION_LEASE_MS = DEFAULT_APPROVAL_CONFIG.executionLeaseMs;
|
|
912
1271
|
function nowIso() {
|
|
913
1272
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
914
1273
|
}
|
|
@@ -945,8 +1304,34 @@ function approvalCommandMatch(prompt, tokenPrefix) {
|
|
|
945
1304
|
}
|
|
946
1305
|
return null;
|
|
947
1306
|
}
|
|
948
|
-
function
|
|
949
|
-
|
|
1307
|
+
function createApprovalRecordWithEnvelope(params) {
|
|
1308
|
+
const envelope = buildReplayEnvelopeFields({
|
|
1309
|
+
kind: params.kind,
|
|
1310
|
+
command: params.approvalInput?.input,
|
|
1311
|
+
input: params.approvalInput?.input,
|
|
1312
|
+
inputKind: params.approvalInput?.inputKind,
|
|
1313
|
+
cwd: params.approvalInput?.cwd,
|
|
1314
|
+
toolName: params.approvalInput?.toolName,
|
|
1315
|
+
payload: params.approvalInput?.payload,
|
|
1316
|
+
fingerprint: params.fingerprint,
|
|
1317
|
+
repoRoot: params.repoRoot
|
|
1318
|
+
});
|
|
1319
|
+
return createApprovalRecord({
|
|
1320
|
+
kind: params.kind,
|
|
1321
|
+
fingerprint: params.fingerprint,
|
|
1322
|
+
repoRoot: params.repoRoot,
|
|
1323
|
+
reason: params.reason,
|
|
1324
|
+
summary: params.summary,
|
|
1325
|
+
approvalTtlMinutes: params.approvalTtlMinutes,
|
|
1326
|
+
approvalId: params.approvalId,
|
|
1327
|
+
input: envelope.input ?? params.approvalInput?.input,
|
|
1328
|
+
inputKind: envelope.inputKind ?? params.approvalInput?.inputKind,
|
|
1329
|
+
cwd: envelope.cwd,
|
|
1330
|
+
toolName: envelope.toolName ?? params.approvalInput?.toolName,
|
|
1331
|
+
payloadHash: envelope.payloadHash,
|
|
1332
|
+
payloadJson: envelope.payloadJson,
|
|
1333
|
+
scopeHint: params.scopeHint
|
|
1334
|
+
});
|
|
950
1335
|
}
|
|
951
1336
|
function createApprovalRecord(params) {
|
|
952
1337
|
const createdAt = nowIso();
|
|
@@ -965,221 +1350,515 @@ function createApprovalRecord(params) {
|
|
|
965
1350
|
record.input = params.input;
|
|
966
1351
|
record.inputKind = params.inputKind ?? params.kind;
|
|
967
1352
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
// src/core/approval-token.ts
|
|
972
|
-
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
973
|
-
import { existsSync } from "node:fs";
|
|
974
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
975
|
-
import path4 from "node:path";
|
|
976
|
-
function base64UrlEncode(value) {
|
|
977
|
-
return Buffer.from(value, "utf8").toString("base64url");
|
|
978
|
-
}
|
|
979
|
-
function base64UrlDecode(value) {
|
|
980
|
-
return Buffer.from(value, "base64url").toString("utf8");
|
|
981
|
-
}
|
|
982
|
-
function approvalSigningKeyPath(controlPlaneDir = defaultControlPlaneDir()) {
|
|
983
|
-
return path4.join(controlPlaneDir, "approval-signing.key");
|
|
984
|
-
}
|
|
985
|
-
async function loadOrCreateApprovalSigningKey(controlPlaneDir = defaultControlPlaneDir()) {
|
|
986
|
-
const keyPath = approvalSigningKeyPath(controlPlaneDir);
|
|
987
|
-
if (existsSync(keyPath)) {
|
|
988
|
-
return readFile(keyPath);
|
|
1353
|
+
if (params.cwd) {
|
|
1354
|
+
record.cwd = params.cwd;
|
|
989
1355
|
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
await writeFile(keyPath, key, { mode: 384 });
|
|
993
|
-
return key;
|
|
994
|
-
}
|
|
995
|
-
function signPayload(payload, key) {
|
|
996
|
-
const body = base64UrlEncode(JSON.stringify(payload));
|
|
997
|
-
const signature = createHmac("sha256", key).update(body).digest("base64url");
|
|
998
|
-
return `${body}.${signature}`;
|
|
999
|
-
}
|
|
1000
|
-
async function issueApprovalToken(payload, controlPlaneDir = defaultControlPlaneDir()) {
|
|
1001
|
-
const key = await loadOrCreateApprovalSigningKey(controlPlaneDir);
|
|
1002
|
-
return signPayload(payload, key);
|
|
1003
|
-
}
|
|
1004
|
-
async function verifyApprovalToken(token, controlPlaneDir = defaultControlPlaneDir()) {
|
|
1005
|
-
const [body, signature] = token.split(".");
|
|
1006
|
-
if (!body || !signature) {
|
|
1007
|
-
return null;
|
|
1356
|
+
if (params.toolName) {
|
|
1357
|
+
record.toolName = params.toolName;
|
|
1008
1358
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
return null;
|
|
1359
|
+
if (params.payloadHash) {
|
|
1360
|
+
record.payloadHash = params.payloadHash;
|
|
1012
1361
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
const actualBuffer = Buffer.from(signature);
|
|
1016
|
-
const expectedBuffer = Buffer.from(expected);
|
|
1017
|
-
if (actualBuffer.length !== expectedBuffer.length || !timingSafeEqual(actualBuffer, expectedBuffer)) {
|
|
1018
|
-
return null;
|
|
1362
|
+
if (params.payloadJson) {
|
|
1363
|
+
record.payloadJson = params.payloadJson;
|
|
1019
1364
|
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
if (Date.parse(payload.expiresAt) <= Date.now()) {
|
|
1023
|
-
return null;
|
|
1024
|
-
}
|
|
1025
|
-
return payload;
|
|
1026
|
-
} catch {
|
|
1027
|
-
return null;
|
|
1365
|
+
if (params.scopeHint) {
|
|
1366
|
+
record.scopeHint = params.scopeHint;
|
|
1028
1367
|
}
|
|
1368
|
+
return record;
|
|
1029
1369
|
}
|
|
1030
1370
|
|
|
1031
|
-
// src/
|
|
1032
|
-
async function recordApproval(params) {
|
|
1033
|
-
const { approvalId, config, store, token, requireSignedToken = false } = params;
|
|
1034
|
-
const pending = await store.loadPending();
|
|
1035
|
-
pending.state = compactApprovals(pending.state);
|
|
1036
|
-
const index = pending.state.approvals.findIndex((approval2) => approval2.approvalId === approvalId);
|
|
1037
|
-
if (index === -1) {
|
|
1038
|
-
await store.writePending(pending.filePath, pending.state);
|
|
1039
|
-
return { ok: false, message: "Belay approval not found or expired." };
|
|
1040
|
-
}
|
|
1041
|
-
const [approval] = pending.state.approvals.slice(index, index + 1);
|
|
1042
|
-
if (requireSignedToken) {
|
|
1043
|
-
if (!token) {
|
|
1044
|
-
return { ok: false, message: "Signed approval token required for out-of-band approval." };
|
|
1045
|
-
}
|
|
1046
|
-
const controlPlaneDir = configuredControlPlaneDir(config);
|
|
1047
|
-
const verified = await verifyApprovalToken(token, controlPlaneDir);
|
|
1048
|
-
if (!verified || verified.approvalId !== approvalId) {
|
|
1049
|
-
return { ok: false, message: "Invalid or expired signed approval token." };
|
|
1050
|
-
}
|
|
1051
|
-
if (verified.fingerprint !== approval.fingerprint || verified.repoRoot !== approval.repoRoot) {
|
|
1052
|
-
return { ok: false, message: "Signed approval token does not match the pending approval." };
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
pending.state.approvals.splice(index, 1);
|
|
1056
|
-
await store.writePending(pending.filePath, pending.state);
|
|
1057
|
-
const approved = await store.loadApproved();
|
|
1058
|
-
approved.state = compactApprovals(approved.state);
|
|
1059
|
-
approved.state.approvals.push({
|
|
1060
|
-
...approval,
|
|
1061
|
-
approvedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1062
|
-
});
|
|
1063
|
-
await store.writeApproved(approved.filePath, approved.state);
|
|
1064
|
-
return {
|
|
1065
|
-
ok: true,
|
|
1066
|
-
message: `Belay approval recorded for ${approvalId}. Retry the original action once before it expires.`,
|
|
1067
|
-
approval
|
|
1068
|
-
};
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
// src/core/capability/allowlist.ts
|
|
1072
|
-
import { existsSync as existsSync3, readFileSync } from "node:fs";
|
|
1371
|
+
// src/adapters/layouts/claude.ts
|
|
1073
1372
|
import path6 from "node:path";
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
import path5 from "node:path";
|
|
1078
|
-
function canonicalPath(targetPath) {
|
|
1079
|
-
const resolved = path5.resolve(targetPath);
|
|
1080
|
-
if (!resolved) {
|
|
1081
|
-
return resolved;
|
|
1082
|
-
}
|
|
1083
|
-
const parsed = path5.parse(resolved);
|
|
1084
|
-
let current = parsed.root;
|
|
1085
|
-
const relativeParts = path5.relative(parsed.root || ".", resolved).split(path5.sep).filter(Boolean);
|
|
1086
|
-
for (let i = 0; i < relativeParts.length; i++) {
|
|
1087
|
-
const segment = relativeParts[i];
|
|
1088
|
-
if (!segment) {
|
|
1089
|
-
continue;
|
|
1090
|
-
}
|
|
1091
|
-
const candidate = current === "" ? segment : path5.join(current, segment);
|
|
1092
|
-
if (!existsSync2(candidate)) {
|
|
1093
|
-
return path5.join(candidate, ...relativeParts.slice(i + 1));
|
|
1094
|
-
}
|
|
1095
|
-
try {
|
|
1096
|
-
current = realpathSync.native(candidate);
|
|
1097
|
-
} catch {
|
|
1098
|
-
return path5.join(candidate, ...relativeParts.slice(i + 1));
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
return current;
|
|
1373
|
+
function runnerCommand2(platform, repoRoot, hookName, ...args) {
|
|
1374
|
+
const hooksDir = path6.join(path6.resolve(repoRoot), ".claude", "hooks");
|
|
1375
|
+
return buildRunnerInvocation(platform, hooksDir, repoRoot, hookName, ...args);
|
|
1102
1376
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1377
|
+
var claudeLayout = {
|
|
1378
|
+
name: "claude",
|
|
1379
|
+
configPath(repoRoot) {
|
|
1380
|
+
return path6.join(repoRoot, ".claude", "belay.config.json");
|
|
1381
|
+
},
|
|
1382
|
+
hooksSettingsPath(repoRoot) {
|
|
1383
|
+
return path6.join(repoRoot, ".claude", "settings.json");
|
|
1384
|
+
},
|
|
1385
|
+
hooksDir(repoRoot) {
|
|
1386
|
+
return path6.join(repoRoot, ".claude", "hooks");
|
|
1387
|
+
},
|
|
1388
|
+
runtimeDir(repoRoot) {
|
|
1389
|
+
return path6.join(repoRoot, ".claude", "belay", "runtime");
|
|
1390
|
+
},
|
|
1391
|
+
repoLocalStateDir(repoRoot) {
|
|
1392
|
+
return path6.join(repoRoot, ".claude", "belay");
|
|
1393
|
+
},
|
|
1394
|
+
defaultAuditLogPath(_repoRoot) {
|
|
1395
|
+
return path6.join(".claude", "belay", "audit.ndjson");
|
|
1396
|
+
},
|
|
1397
|
+
repoRootMarkers: [".git", ".claude"],
|
|
1398
|
+
runnerCommand: runnerCommand2,
|
|
1399
|
+
defaultConfig(repoRoot) {
|
|
1400
|
+
return {
|
|
1401
|
+
...DEFAULT_CONFIG_V4,
|
|
1402
|
+
adapter: "claude",
|
|
1403
|
+
audit: {
|
|
1404
|
+
...DEFAULT_CONFIG_V4.audit,
|
|
1405
|
+
logPath: claudeLayout.defaultAuditLogPath(repoRoot)
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1109
1408
|
}
|
|
1110
|
-
|
|
1409
|
+
};
|
|
1410
|
+
|
|
1411
|
+
// src/adapters/layouts/codex.ts
|
|
1412
|
+
import path7 from "node:path";
|
|
1413
|
+
function runnerCommand3(platform, repoRoot, hookName, ...args) {
|
|
1414
|
+
const hooksDir = path7.join(path7.resolve(repoRoot), ".codex", "hooks");
|
|
1415
|
+
return buildRunnerInvocation(platform, hooksDir, repoRoot, hookName, ...args);
|
|
1111
1416
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1417
|
+
var codexLayout = {
|
|
1418
|
+
name: "codex",
|
|
1419
|
+
configPath(repoRoot) {
|
|
1420
|
+
return path7.join(repoRoot, ".codex", "belay.config.json");
|
|
1421
|
+
},
|
|
1422
|
+
// Codex reads lifecycle hooks from `.codex/config.toml` (project layer).
|
|
1423
|
+
hooksSettingsPath(repoRoot) {
|
|
1424
|
+
return path7.join(repoRoot, ".codex", "config.toml");
|
|
1425
|
+
},
|
|
1426
|
+
hooksDir(repoRoot) {
|
|
1427
|
+
return path7.join(repoRoot, ".codex", "hooks");
|
|
1428
|
+
},
|
|
1429
|
+
runtimeDir(repoRoot) {
|
|
1430
|
+
return path7.join(repoRoot, ".codex", "belay", "runtime");
|
|
1431
|
+
},
|
|
1432
|
+
repoLocalStateDir(repoRoot) {
|
|
1433
|
+
return path7.join(repoRoot, ".codex", "belay");
|
|
1434
|
+
},
|
|
1435
|
+
defaultAuditLogPath(_repoRoot) {
|
|
1436
|
+
return path7.join(".codex", "belay", "audit.ndjson");
|
|
1437
|
+
},
|
|
1438
|
+
repoRootMarkers: [".git", ".codex"],
|
|
1439
|
+
runnerCommand: runnerCommand3,
|
|
1440
|
+
defaultConfig(repoRoot) {
|
|
1441
|
+
return {
|
|
1442
|
+
...DEFAULT_CONFIG_V4,
|
|
1443
|
+
adapter: "codex",
|
|
1444
|
+
audit: {
|
|
1445
|
+
...DEFAULT_CONFIG_V4.audit,
|
|
1446
|
+
logPath: codexLayout.defaultAuditLogPath(repoRoot)
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1121
1449
|
}
|
|
1122
|
-
|
|
1450
|
+
};
|
|
1451
|
+
|
|
1452
|
+
// src/adapters/layouts/index.ts
|
|
1453
|
+
var layouts = {
|
|
1454
|
+
cursor: cursorLayout,
|
|
1455
|
+
claude: claudeLayout,
|
|
1456
|
+
codex: codexLayout
|
|
1457
|
+
};
|
|
1458
|
+
function getAdapterLayout(name = "cursor") {
|
|
1459
|
+
return layouts[name];
|
|
1123
1460
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1461
|
+
|
|
1462
|
+
// src/core/config-layers.ts
|
|
1463
|
+
import path8 from "node:path";
|
|
1464
|
+
|
|
1465
|
+
// src/presets.ts
|
|
1466
|
+
var CONFIG_PRESETS = {
|
|
1467
|
+
strict: {
|
|
1468
|
+
mode: "enforce",
|
|
1469
|
+
policy: {
|
|
1470
|
+
...DEFAULT_CONFIG_V3.policy,
|
|
1471
|
+
unknownLocalEffect: "deny",
|
|
1472
|
+
unparseableShell: "deny",
|
|
1473
|
+
confidenceThresholds: { allow: 0.9, flag: 0.8 },
|
|
1474
|
+
modelAssist: { enabled: false }
|
|
1475
|
+
},
|
|
1476
|
+
sandbox: { ...DEFAULT_CONFIG_V3.sandbox }
|
|
1477
|
+
},
|
|
1478
|
+
standard: {
|
|
1479
|
+
mode: "enforce"
|
|
1480
|
+
},
|
|
1481
|
+
"audit-first": {
|
|
1482
|
+
mode: "audit",
|
|
1483
|
+
policy: {
|
|
1484
|
+
...DEFAULT_CONFIG_V3.policy,
|
|
1485
|
+
unknownLocalEffect: "deny",
|
|
1486
|
+
unparseableShell: "deny",
|
|
1487
|
+
confidenceThresholds: { allow: 0.88, flag: 0.72 },
|
|
1488
|
+
modelAssist: { enabled: false }
|
|
1489
|
+
},
|
|
1490
|
+
sandbox: { ...DEFAULT_CONFIG_V3.sandbox }
|
|
1491
|
+
},
|
|
1492
|
+
"l1-full-recommended": {
|
|
1493
|
+
mode: "enforce",
|
|
1494
|
+
policy: {
|
|
1495
|
+
...DEFAULT_CONFIG_V3.policy,
|
|
1496
|
+
confidenceThresholds: { ...DEFAULT_CONFIG_V3.policy.confidenceThresholds },
|
|
1497
|
+
modelAssist: { ...DEFAULT_CONFIG_V3.policy.modelAssist }
|
|
1498
|
+
},
|
|
1499
|
+
sandbox: {
|
|
1500
|
+
enabled: true,
|
|
1501
|
+
runtime: "container",
|
|
1502
|
+
denyNetworkByDefault: true
|
|
1503
|
+
},
|
|
1504
|
+
egress: {
|
|
1505
|
+
...DEFAULT_CONFIG_V3.egress,
|
|
1506
|
+
enabled: true,
|
|
1507
|
+
demoteL3External: true
|
|
1508
|
+
},
|
|
1509
|
+
approvalSigning: {
|
|
1510
|
+
required: true
|
|
1511
|
+
},
|
|
1512
|
+
controlPlane: {
|
|
1513
|
+
...DEFAULT_CONFIG_V3.controlPlane,
|
|
1514
|
+
isolation: {
|
|
1515
|
+
mode: "separate-user",
|
|
1516
|
+
verifyAgentWritable: true
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1139
1519
|
}
|
|
1140
|
-
|
|
1520
|
+
};
|
|
1521
|
+
function applyConfigPreset(preset, extra = {}) {
|
|
1522
|
+
const base = CONFIG_PRESETS[preset] ?? CONFIG_PRESETS.standard;
|
|
1523
|
+
return {
|
|
1524
|
+
version: 3,
|
|
1525
|
+
...base,
|
|
1526
|
+
...extra,
|
|
1527
|
+
policy: {
|
|
1528
|
+
...DEFAULT_CONFIG_V3.policy,
|
|
1529
|
+
...base.policy ?? {},
|
|
1530
|
+
...extra.policy
|
|
1531
|
+
},
|
|
1532
|
+
sandbox: {
|
|
1533
|
+
...DEFAULT_CONFIG_V3.sandbox,
|
|
1534
|
+
...base.sandbox ?? {},
|
|
1535
|
+
...extra.sandbox
|
|
1536
|
+
},
|
|
1537
|
+
egress: {
|
|
1538
|
+
...DEFAULT_CONFIG_V3.egress,
|
|
1539
|
+
...base.egress ?? {},
|
|
1540
|
+
...extra.egress
|
|
1541
|
+
},
|
|
1542
|
+
approvalSigning: {
|
|
1543
|
+
...DEFAULT_CONFIG_V3.approvalSigning,
|
|
1544
|
+
...base.approvalSigning ?? {},
|
|
1545
|
+
...extra.approvalSigning
|
|
1546
|
+
},
|
|
1547
|
+
controlPlane: {
|
|
1548
|
+
...DEFAULT_CONFIG_V3.controlPlane,
|
|
1549
|
+
...base.controlPlane ?? {},
|
|
1550
|
+
...extra.controlPlane,
|
|
1551
|
+
isolation: {
|
|
1552
|
+
...DEFAULT_CONFIG_V3.controlPlane.isolation,
|
|
1553
|
+
...base.controlPlane?.isolation ?? {},
|
|
1554
|
+
...extra.controlPlane?.isolation ?? {}
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
};
|
|
1141
1558
|
}
|
|
1142
1559
|
|
|
1143
|
-
// src/core/
|
|
1144
|
-
function
|
|
1145
|
-
|
|
1560
|
+
// src/core/config-layers.ts
|
|
1561
|
+
function teamConfigPath(homedir = () => process.env.HOME ?? process.env.USERPROFILE ?? "") {
|
|
1562
|
+
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
1563
|
+
const base = xdg || path8.join(homedir(), ".config");
|
|
1564
|
+
return path8.join(base, "agent-belay", "team.config.json");
|
|
1146
1565
|
}
|
|
1147
|
-
function
|
|
1148
|
-
|
|
1149
|
-
|
|
1566
|
+
function applyProtectedLayer(config, builtin) {
|
|
1567
|
+
const controlPlane = { ...config.controlPlane };
|
|
1568
|
+
if (builtin.controlPlane.enabled && controlPlane.enabled === false) {
|
|
1569
|
+
controlPlane.enabled = true;
|
|
1570
|
+
}
|
|
1571
|
+
if (builtin.controlPlane.integrity === "hash-pinned" && controlPlane.integrity === "none") {
|
|
1572
|
+
controlPlane.integrity = "hash-pinned";
|
|
1150
1573
|
}
|
|
1151
|
-
const raw = JSON.parse(readFileSync(filePath, "utf8"));
|
|
1152
1574
|
return {
|
|
1153
|
-
|
|
1154
|
-
|
|
1575
|
+
...config,
|
|
1576
|
+
controlPlane
|
|
1155
1577
|
};
|
|
1156
1578
|
}
|
|
1157
|
-
function
|
|
1158
|
-
|
|
1579
|
+
function asV3Layer(raw) {
|
|
1580
|
+
if (!raw || typeof raw !== "object") {
|
|
1581
|
+
return { version: 3 };
|
|
1582
|
+
}
|
|
1583
|
+
return { version: 3, ...raw };
|
|
1159
1584
|
}
|
|
1160
|
-
function
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1585
|
+
function mergeConfigLayer(base, layer) {
|
|
1586
|
+
const merged = mergeConfig(layer, base);
|
|
1587
|
+
if (!layer.policy) {
|
|
1588
|
+
return { ...merged, policy: base.policy };
|
|
1589
|
+
}
|
|
1590
|
+
return merged;
|
|
1166
1591
|
}
|
|
1167
|
-
function
|
|
1168
|
-
|
|
1592
|
+
function resolveLayeredConfig(params) {
|
|
1593
|
+
const provenance = [{ path: "(builtin)", source: "builtin" }];
|
|
1594
|
+
let config = mergeConfig({}, params.adapterDefaults);
|
|
1595
|
+
if (params.teamConfig) {
|
|
1596
|
+
const teamFile = params.teamConfig;
|
|
1597
|
+
const teamRaw = teamFile.preset ? applyConfigPreset(teamFile.preset, teamFile.config ?? {}) : teamFile.config ?? params.teamConfig;
|
|
1598
|
+
rejectTeamLayerJudgeSecrets(
|
|
1599
|
+
teamRaw.judge,
|
|
1600
|
+
"team"
|
|
1601
|
+
);
|
|
1602
|
+
config = mergeConfigLayer(config, asV3Layer(teamRaw));
|
|
1603
|
+
provenance.push({
|
|
1604
|
+
path: params.teamConfigPath ?? teamConfigPath(),
|
|
1605
|
+
source: "team"
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
config = mergeConfigLayer(config, asV3Layer(params.repoConfig));
|
|
1609
|
+
if (params.repoConfigPath) {
|
|
1610
|
+
provenance.push({ path: params.repoConfigPath, source: "repo" });
|
|
1611
|
+
}
|
|
1612
|
+
const protectedConfig = applyProtectedLayer(config, DEFAULT_CONFIG_V3);
|
|
1613
|
+
if (JSON.stringify(protectedConfig) !== JSON.stringify(config)) {
|
|
1614
|
+
provenance.push({ path: "(protected-layer)", source: "protected" });
|
|
1615
|
+
config = protectedConfig;
|
|
1616
|
+
}
|
|
1617
|
+
return { config, provenance };
|
|
1169
1618
|
}
|
|
1170
1619
|
|
|
1171
|
-
// src/
|
|
1172
|
-
function
|
|
1173
|
-
|
|
1620
|
+
// src/config-io.ts
|
|
1621
|
+
function resolveAdapterName(config) {
|
|
1622
|
+
if (config.adapter === "claude") {
|
|
1623
|
+
return "claude";
|
|
1624
|
+
}
|
|
1625
|
+
if (config.adapter === "codex") {
|
|
1626
|
+
return "codex";
|
|
1627
|
+
}
|
|
1628
|
+
return "cursor";
|
|
1174
1629
|
}
|
|
1175
|
-
function
|
|
1176
|
-
return
|
|
1630
|
+
function repoLocalStateDirFor(repoRoot, config) {
|
|
1631
|
+
return getAdapterLayout(resolveAdapterName(config)).repoLocalStateDir(repoRoot);
|
|
1177
1632
|
}
|
|
1178
1633
|
|
|
1179
|
-
// src/core/
|
|
1180
|
-
import
|
|
1634
|
+
// src/core/approval-token.ts
|
|
1635
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
1636
|
+
import { existsSync } from "node:fs";
|
|
1637
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1638
|
+
import path9 from "node:path";
|
|
1639
|
+
function base64UrlEncode(value) {
|
|
1640
|
+
return Buffer.from(value, "utf8").toString("base64url");
|
|
1641
|
+
}
|
|
1642
|
+
function base64UrlDecode(value) {
|
|
1643
|
+
return Buffer.from(value, "base64url").toString("utf8");
|
|
1644
|
+
}
|
|
1645
|
+
function approvalSigningKeyPath(controlPlaneDir = defaultControlPlaneDir()) {
|
|
1646
|
+
return path9.join(controlPlaneDir, "approval-signing.key");
|
|
1647
|
+
}
|
|
1648
|
+
async function loadOrCreateApprovalSigningKey(controlPlaneDir = defaultControlPlaneDir()) {
|
|
1649
|
+
const keyPath = approvalSigningKeyPath(controlPlaneDir);
|
|
1650
|
+
if (existsSync(keyPath)) {
|
|
1651
|
+
return readFile(keyPath);
|
|
1652
|
+
}
|
|
1653
|
+
await mkdir(controlPlaneDir, { recursive: true });
|
|
1654
|
+
const key = randomBytes(32);
|
|
1655
|
+
await writeFile(keyPath, key, { mode: 384 });
|
|
1656
|
+
return key;
|
|
1657
|
+
}
|
|
1658
|
+
function signPayload(payload, key) {
|
|
1659
|
+
const body = base64UrlEncode(JSON.stringify(payload));
|
|
1660
|
+
const signature = createHmac("sha256", key).update(body).digest("base64url");
|
|
1661
|
+
return `${body}.${signature}`;
|
|
1662
|
+
}
|
|
1663
|
+
async function issueApprovalToken(payload, controlPlaneDir = defaultControlPlaneDir()) {
|
|
1664
|
+
const key = await loadOrCreateApprovalSigningKey(controlPlaneDir);
|
|
1665
|
+
return signPayload(payload, key);
|
|
1666
|
+
}
|
|
1667
|
+
async function verifyApprovalToken(token, controlPlaneDir = defaultControlPlaneDir()) {
|
|
1668
|
+
const [body, signature] = token.split(".");
|
|
1669
|
+
if (!body || !signature) {
|
|
1670
|
+
return null;
|
|
1671
|
+
}
|
|
1672
|
+
const keyPath = approvalSigningKeyPath(controlPlaneDir);
|
|
1673
|
+
if (!existsSync(keyPath)) {
|
|
1674
|
+
return null;
|
|
1675
|
+
}
|
|
1676
|
+
const key = await readFile(keyPath);
|
|
1677
|
+
const expected = createHmac("sha256", key).update(body).digest("base64url");
|
|
1678
|
+
const actualBuffer = Buffer.from(signature);
|
|
1679
|
+
const expectedBuffer = Buffer.from(expected);
|
|
1680
|
+
if (actualBuffer.length !== expectedBuffer.length || !timingSafeEqual(actualBuffer, expectedBuffer)) {
|
|
1681
|
+
return null;
|
|
1682
|
+
}
|
|
1683
|
+
try {
|
|
1684
|
+
const payload = JSON.parse(base64UrlDecode(body));
|
|
1685
|
+
if (Date.parse(payload.expiresAt) <= Date.now()) {
|
|
1686
|
+
return null;
|
|
1687
|
+
}
|
|
1688
|
+
return payload;
|
|
1689
|
+
} catch {
|
|
1690
|
+
return null;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// src/core/approval-service.ts
|
|
1695
|
+
async function recordApproval(params) {
|
|
1696
|
+
const { approvalId, config, store, token, requireSignedToken = false, adapter } = params;
|
|
1697
|
+
const pending = await store.loadPending();
|
|
1698
|
+
pending.state = compactApprovals(pending.state);
|
|
1699
|
+
const index = pending.state.approvals.findIndex((approval2) => approval2.approvalId === approvalId);
|
|
1700
|
+
if (index === -1) {
|
|
1701
|
+
await store.writePending(pending.filePath, pending.state);
|
|
1702
|
+
return { ok: false, message: "Belay approval not found or expired." };
|
|
1703
|
+
}
|
|
1704
|
+
const [approval] = pending.state.approvals.slice(index, index + 1);
|
|
1705
|
+
if (requireSignedToken) {
|
|
1706
|
+
if (!token) {
|
|
1707
|
+
return { ok: false, message: "Signed approval token required for out-of-band approval." };
|
|
1708
|
+
}
|
|
1709
|
+
const controlPlaneDir = configuredControlPlaneDir(config);
|
|
1710
|
+
const verified = await verifyApprovalToken(token, controlPlaneDir);
|
|
1711
|
+
if (!verified || verified.approvalId !== approvalId) {
|
|
1712
|
+
return { ok: false, message: "Invalid or expired signed approval token." };
|
|
1713
|
+
}
|
|
1714
|
+
if (verified.fingerprint !== approval.fingerprint || verified.repoRoot !== approval.repoRoot) {
|
|
1715
|
+
return { ok: false, message: "Signed approval token does not match the pending approval." };
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
pending.state.approvals.splice(index, 1);
|
|
1719
|
+
await store.writePending(pending.filePath, pending.state);
|
|
1720
|
+
const approved = await store.loadApproved();
|
|
1721
|
+
approved.state = compactApprovals(approved.state);
|
|
1722
|
+
approved.state.approvals.push({
|
|
1723
|
+
...approval,
|
|
1724
|
+
approvedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1725
|
+
});
|
|
1726
|
+
await store.writeApproved(approved.filePath, approved.state);
|
|
1727
|
+
return {
|
|
1728
|
+
ok: true,
|
|
1729
|
+
message: buildApprovalRecordedMessage(config, approval, adapter),
|
|
1730
|
+
approval
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
async function consumeApprovedAfterCliReplay(params) {
|
|
1734
|
+
const approved = await params.store.loadApproved();
|
|
1735
|
+
approved.state = compactApprovals(approved.state);
|
|
1736
|
+
const remaining = approved.state.approvals.filter(
|
|
1737
|
+
(approval) => approval.approvalId !== params.approvalId
|
|
1738
|
+
);
|
|
1739
|
+
if (remaining.length === approved.state.approvals.length) {
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
approved.state.approvals = remaining;
|
|
1743
|
+
await params.store.writeApproved(approved.filePath, approved.state);
|
|
1744
|
+
}
|
|
1745
|
+
function gateApprovalStoreFromDeps(deps) {
|
|
1746
|
+
return {
|
|
1747
|
+
loadPending: () => deps.loadApprovals("pending-approvals.json"),
|
|
1748
|
+
loadApproved: () => deps.loadApprovals("approved-approvals.json"),
|
|
1749
|
+
writePending: (filePath, state) => deps.writeApprovals(filePath, state),
|
|
1750
|
+
writeApproved: (filePath, state) => deps.writeApprovals(filePath, state)
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// src/core/audit-replay-context.ts
|
|
1755
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1756
|
+
function hashReplayPayload(payload) {
|
|
1757
|
+
return createHash2("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
1758
|
+
}
|
|
1759
|
+
function buildAuditActionSnapshot(kind, result, replayAction) {
|
|
1760
|
+
if (!replayAction?.cwd) {
|
|
1761
|
+
return void 0;
|
|
1762
|
+
}
|
|
1763
|
+
const replayKind = replayAction.kind;
|
|
1764
|
+
const resolvedKind = replayKind === "shell" || replayKind === "tool" || replayKind === "subagent" ? replayKind : kind;
|
|
1765
|
+
const normalizedAction = result.normalizedCommand ?? replayAction.command ?? result.summary ?? "";
|
|
1766
|
+
if (!normalizedAction.trim()) {
|
|
1767
|
+
return void 0;
|
|
1768
|
+
}
|
|
1769
|
+
return {
|
|
1770
|
+
schemaVersion: 1,
|
|
1771
|
+
kind: resolvedKind,
|
|
1772
|
+
cwd: replayAction.cwd,
|
|
1773
|
+
normalizedAction,
|
|
1774
|
+
...replayAction.toolName ? { toolName: replayAction.toolName } : {},
|
|
1775
|
+
...replayAction.payload ? { payloadHash: hashReplayPayload(replayAction.payload) } : {}
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
function buildAuditReplayContext(kind, result, replayAction) {
|
|
1779
|
+
if (!replayAction?.cwd) {
|
|
1780
|
+
return void 0;
|
|
1781
|
+
}
|
|
1782
|
+
const replayKind = replayAction.kind;
|
|
1783
|
+
const resolvedKind = replayKind === "shell" || replayKind === "tool" || replayKind === "subagent" ? replayKind : kind;
|
|
1784
|
+
return {
|
|
1785
|
+
cwd: replayAction.cwd,
|
|
1786
|
+
kind: resolvedKind,
|
|
1787
|
+
command: replayAction.command ?? result.normalizedCommand ?? result.summary,
|
|
1788
|
+
...replayAction.toolName ? { toolName: replayAction.toolName } : {},
|
|
1789
|
+
...replayAction.payload ? { payload: replayAction.payload } : {}
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// src/core/capability/allowlist.ts
|
|
1794
|
+
import { existsSync as existsSync3, readFileSync } from "node:fs";
|
|
1795
|
+
import path11 from "node:path";
|
|
1796
|
+
|
|
1797
|
+
// src/core/path-utils.ts
|
|
1798
|
+
import { existsSync as existsSync2, realpathSync, statSync } from "node:fs";
|
|
1799
|
+
import path10 from "node:path";
|
|
1181
1800
|
|
|
1182
1801
|
// src/core/shell-tokenizer.ts
|
|
1802
|
+
var FD_DUPLICATION_PATTERN = /^\d+[<>]&(?:\d+|-)$/;
|
|
1803
|
+
var FD_REDIRECT_PATTERN = /^\d+(?:>>?|<)$/;
|
|
1804
|
+
function readDigits(input, index) {
|
|
1805
|
+
let end = index;
|
|
1806
|
+
while (end < input.length && /[0-9]/.test(input[end] ?? "")) {
|
|
1807
|
+
end += 1;
|
|
1808
|
+
}
|
|
1809
|
+
return input.slice(index, end);
|
|
1810
|
+
}
|
|
1811
|
+
function readShellOperator(input, index) {
|
|
1812
|
+
const char = input[index];
|
|
1813
|
+
const next = input[index + 1] ?? "";
|
|
1814
|
+
const digits = /[0-9]/.test(char) ? readDigits(input, index) : "";
|
|
1815
|
+
const digitsLength = digits.length;
|
|
1816
|
+
const afterDigits = digitsLength > 0 ? input[index + digitsLength] ?? "" : "";
|
|
1817
|
+
const afterDigitsNext = digitsLength > 0 ? input[index + digitsLength + 1] ?? "" : "";
|
|
1818
|
+
if (char === "&" && next === "&") {
|
|
1819
|
+
return { token: "&&", length: 2 };
|
|
1820
|
+
}
|
|
1821
|
+
if (char === "|" && next === "|") {
|
|
1822
|
+
return { token: "||", length: 2 };
|
|
1823
|
+
}
|
|
1824
|
+
if (char === "|" && next === "&") {
|
|
1825
|
+
return { token: "|&", length: 2 };
|
|
1826
|
+
}
|
|
1827
|
+
if (char === "&" && next === ">") {
|
|
1828
|
+
return { token: "&>", length: 2 };
|
|
1829
|
+
}
|
|
1830
|
+
if (digitsLength > 0 && (afterDigits === ">" || afterDigits === "<")) {
|
|
1831
|
+
if (afterDigitsNext === "&") {
|
|
1832
|
+
let end = index + digitsLength + 2;
|
|
1833
|
+
while (end < input.length && /[0-9-]/.test(input[end] ?? "")) {
|
|
1834
|
+
end += 1;
|
|
1835
|
+
}
|
|
1836
|
+
if (end > index + digitsLength + 2) {
|
|
1837
|
+
return { token: input.slice(index, end), length: end - index };
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
if (afterDigits === ">" && afterDigitsNext === ">") {
|
|
1841
|
+
return { token: `${digits}>>`, length: digitsLength + 2 };
|
|
1842
|
+
}
|
|
1843
|
+
return { token: `${digits}${afterDigits}`, length: digitsLength + 1 };
|
|
1844
|
+
}
|
|
1845
|
+
if (char === ">" && next === ">") {
|
|
1846
|
+
return { token: ">>", length: 2 };
|
|
1847
|
+
}
|
|
1848
|
+
if (char === "&") {
|
|
1849
|
+
return { token: "&", length: 1 };
|
|
1850
|
+
}
|
|
1851
|
+
if (char === "|" || char === ";" || char === ">" || char === "<") {
|
|
1852
|
+
return { token: char, length: 1 };
|
|
1853
|
+
}
|
|
1854
|
+
return null;
|
|
1855
|
+
}
|
|
1856
|
+
function isRedirectOperator(token) {
|
|
1857
|
+
return token === ">" || token === ">>" || token === "<" || token === "&>" || FD_REDIRECT_PATTERN.test(token);
|
|
1858
|
+
}
|
|
1859
|
+
function isFdDuplication(token) {
|
|
1860
|
+
return FD_DUPLICATION_PATTERN.test(token);
|
|
1861
|
+
}
|
|
1183
1862
|
function tokenizeShell(input) {
|
|
1184
1863
|
const tokens = [];
|
|
1185
1864
|
let buffer = "";
|
|
@@ -1193,7 +1872,6 @@ function tokenizeShell(input) {
|
|
|
1193
1872
|
};
|
|
1194
1873
|
for (let index = 0; index < input.length; index += 1) {
|
|
1195
1874
|
const char = input[index];
|
|
1196
|
-
const next = input[index + 1] ?? "";
|
|
1197
1875
|
if (escaping) {
|
|
1198
1876
|
buffer += char;
|
|
1199
1877
|
escaping = false;
|
|
@@ -1215,27 +1893,11 @@ function tokenizeShell(input) {
|
|
|
1215
1893
|
quote = char;
|
|
1216
1894
|
continue;
|
|
1217
1895
|
}
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
tokens.push("&&");
|
|
1221
|
-
index += 1;
|
|
1222
|
-
continue;
|
|
1223
|
-
}
|
|
1224
|
-
if (char === "|" && next === "|") {
|
|
1225
|
-
flush();
|
|
1226
|
-
tokens.push("||");
|
|
1227
|
-
index += 1;
|
|
1228
|
-
continue;
|
|
1229
|
-
}
|
|
1230
|
-
if (char === ">" && next === ">") {
|
|
1231
|
-
flush();
|
|
1232
|
-
tokens.push(">>");
|
|
1233
|
-
index += 1;
|
|
1234
|
-
continue;
|
|
1235
|
-
}
|
|
1236
|
-
if (char === "|" || char === ";" || char === ">" || char === "<") {
|
|
1896
|
+
const operator = readShellOperator(input, index);
|
|
1897
|
+
if (operator) {
|
|
1237
1898
|
flush();
|
|
1238
|
-
tokens.push(
|
|
1899
|
+
tokens.push(operator.token);
|
|
1900
|
+
index += operator.length - 1;
|
|
1239
1901
|
continue;
|
|
1240
1902
|
}
|
|
1241
1903
|
if (char === "\n" || char === "\r") {
|
|
@@ -1265,7 +1927,10 @@ function extractRedirectTargets(tokens) {
|
|
|
1265
1927
|
const targets = [];
|
|
1266
1928
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
1267
1929
|
const token = tokens[index];
|
|
1268
|
-
if (token
|
|
1930
|
+
if (isFdDuplication(token)) {
|
|
1931
|
+
continue;
|
|
1932
|
+
}
|
|
1933
|
+
if (isRedirectOperator(token)) {
|
|
1269
1934
|
const next = tokens[index + 1];
|
|
1270
1935
|
if (next) {
|
|
1271
1936
|
targets.push(next);
|
|
@@ -1275,73 +1940,215 @@ function extractRedirectTargets(tokens) {
|
|
|
1275
1940
|
return targets;
|
|
1276
1941
|
}
|
|
1277
1942
|
|
|
1278
|
-
// src/core/
|
|
1279
|
-
function
|
|
1280
|
-
const
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1943
|
+
// src/core/path-utils.ts
|
|
1944
|
+
function canonicalPath(targetPath) {
|
|
1945
|
+
const resolved = path10.resolve(targetPath);
|
|
1946
|
+
if (!resolved) {
|
|
1947
|
+
return resolved;
|
|
1948
|
+
}
|
|
1949
|
+
const parsed = path10.parse(resolved);
|
|
1950
|
+
let current = parsed.root;
|
|
1951
|
+
const relativeParts = path10.relative(parsed.root || ".", resolved).split(path10.sep).filter(Boolean);
|
|
1952
|
+
for (let i = 0; i < relativeParts.length; i++) {
|
|
1953
|
+
const segment = relativeParts[i];
|
|
1954
|
+
if (!segment) {
|
|
1285
1955
|
continue;
|
|
1286
1956
|
}
|
|
1287
|
-
const
|
|
1288
|
-
if (
|
|
1289
|
-
|
|
1957
|
+
const candidate = current === "" ? segment : path10.join(current, segment);
|
|
1958
|
+
if (!existsSync2(candidate)) {
|
|
1959
|
+
return path10.join(candidate, ...relativeParts.slice(i + 1));
|
|
1960
|
+
}
|
|
1961
|
+
try {
|
|
1962
|
+
current = realpathSync.native(candidate);
|
|
1963
|
+
} catch {
|
|
1964
|
+
return path10.join(candidate, ...relativeParts.slice(i + 1));
|
|
1290
1965
|
}
|
|
1291
1966
|
}
|
|
1292
|
-
return
|
|
1967
|
+
return current;
|
|
1293
1968
|
}
|
|
1294
|
-
function
|
|
1295
|
-
const
|
|
1296
|
-
|
|
1969
|
+
function pathWithinRoot(root, targetPath) {
|
|
1970
|
+
const resolvedRoot = canonicalPath(root);
|
|
1971
|
+
const resolvedTarget = canonicalPath(targetPath);
|
|
1972
|
+
const relativePath = path10.relative(resolvedRoot, resolvedTarget);
|
|
1973
|
+
if (relativePath === "") {
|
|
1974
|
+
return true;
|
|
1975
|
+
}
|
|
1976
|
+
return !relativePath.startsWith("..") && !path10.isAbsolute(relativePath);
|
|
1977
|
+
}
|
|
1978
|
+
function relativeWithinRoot(root, targetPath, options = {}) {
|
|
1979
|
+
const resolvedRoot = options.canonicalizeRoot === false ? path10.resolve(root) : canonicalPath(root);
|
|
1980
|
+
const resolvedTarget = canonicalPath(targetPath);
|
|
1981
|
+
const relativePath = path10.relative(resolvedRoot, resolvedTarget);
|
|
1982
|
+
if (relativePath === "") {
|
|
1983
|
+
return ".";
|
|
1984
|
+
}
|
|
1985
|
+
if (relativePath.startsWith("..") || path10.isAbsolute(relativePath)) {
|
|
1297
1986
|
return null;
|
|
1298
1987
|
}
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1988
|
+
return relativePath;
|
|
1989
|
+
}
|
|
1990
|
+
function relativeWithinRepo(repoRoot, targetPath) {
|
|
1991
|
+
return relativeWithinRoot(repoRoot, targetPath);
|
|
1992
|
+
}
|
|
1993
|
+
function resolveWorkspaceRootMatch(repoRoot, trustedRoots = [], targetPath) {
|
|
1994
|
+
const canonicalRepoRoot = canonicalPath(repoRoot);
|
|
1995
|
+
const repoRelative = relativeWithinRoot(repoRoot, targetPath);
|
|
1996
|
+
if (repoRelative !== null) {
|
|
1997
|
+
return { kind: "repo", root: canonicalRepoRoot, relativePath: repoRelative };
|
|
1998
|
+
}
|
|
1999
|
+
const normalizedTrustedRoots = [...new Set(trustedRoots.map((root) => path10.resolve(root)))].filter((root) => root !== canonicalRepoRoot).sort((left, right) => right.length - left.length);
|
|
2000
|
+
for (const root of normalizedTrustedRoots) {
|
|
2001
|
+
const trustedRelative = relativeWithinRoot(root, targetPath, { canonicalizeRoot: false });
|
|
2002
|
+
if (trustedRelative !== null) {
|
|
2003
|
+
return { kind: "trusted", root, relativePath: trustedRelative };
|
|
1303
2004
|
}
|
|
1304
2005
|
}
|
|
1305
2006
|
return null;
|
|
1306
2007
|
}
|
|
1307
|
-
function
|
|
1308
|
-
|
|
1309
|
-
if (!toolInput || typeof toolInput !== "object") {
|
|
2008
|
+
function resolveMutationTarget(token, cwd) {
|
|
2009
|
+
if (!token || token === "--" || token.startsWith("-")) {
|
|
1310
2010
|
return null;
|
|
1311
2011
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
if (typeof input[key] === "string" && input[key].trim()) {
|
|
1315
|
-
return input[key];
|
|
1316
|
-
}
|
|
2012
|
+
if (isRedirectOperator(token) || isFdDuplication(token) || token === "&&" || token === "||" || token === "|" || token === "|&" || token === ";" || token === "&") {
|
|
2013
|
+
return null;
|
|
1317
2014
|
}
|
|
1318
|
-
|
|
2015
|
+
if (path10.isAbsolute(token)) {
|
|
2016
|
+
return canonicalPath(token);
|
|
2017
|
+
}
|
|
2018
|
+
if (token.startsWith("./") || token.startsWith("../")) {
|
|
2019
|
+
return canonicalPath(path10.resolve(cwd, token));
|
|
2020
|
+
}
|
|
2021
|
+
if (!token.includes("/") && !token.includes("\\")) {
|
|
2022
|
+
return canonicalPath(path10.resolve(cwd, token));
|
|
2023
|
+
}
|
|
2024
|
+
return canonicalPath(path10.resolve(cwd, token));
|
|
2025
|
+
}
|
|
2026
|
+
function containingGitRoot(targetPath) {
|
|
2027
|
+
const resolved = canonicalPath(targetPath);
|
|
2028
|
+
let current = resolved;
|
|
2029
|
+
try {
|
|
2030
|
+
if (!statSync(current).isDirectory()) {
|
|
2031
|
+
current = path10.dirname(current);
|
|
2032
|
+
}
|
|
2033
|
+
} catch {
|
|
2034
|
+
current = path10.dirname(current);
|
|
2035
|
+
}
|
|
2036
|
+
while (true) {
|
|
2037
|
+
if (existsSync2(path10.join(current, ".git"))) {
|
|
2038
|
+
return current;
|
|
2039
|
+
}
|
|
2040
|
+
const parent = path10.dirname(current);
|
|
2041
|
+
if (parent === current) {
|
|
2042
|
+
return null;
|
|
2043
|
+
}
|
|
2044
|
+
current = parent;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
// src/core/capability/allowlist.ts
|
|
2049
|
+
function fsScopeAllowlistPath(config, repoLocalStateDir) {
|
|
2050
|
+
return path11.join(belayStateDir(config, repoLocalStateDir), "fs-scope-allowlist.json");
|
|
2051
|
+
}
|
|
2052
|
+
function loadFsScopeAllowlistSync(filePath) {
|
|
2053
|
+
if (!existsSync3(filePath)) {
|
|
2054
|
+
return { version: 1, paths: [] };
|
|
2055
|
+
}
|
|
2056
|
+
const raw = JSON.parse(readFileSync(filePath, "utf8"));
|
|
2057
|
+
return {
|
|
2058
|
+
version: 1,
|
|
2059
|
+
paths: Array.isArray(raw.paths) ? raw.paths : []
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
function normalizeAllowlistPath(targetPath) {
|
|
2063
|
+
return canonicalPath(targetPath);
|
|
2064
|
+
}
|
|
2065
|
+
function isPathAllowlisted(absolutePath, allowlist) {
|
|
2066
|
+
const resolved = normalizeAllowlistPath(absolutePath);
|
|
2067
|
+
return allowlist.paths.some((entry) => {
|
|
2068
|
+
const scope = normalizeAllowlistPath(entry.path);
|
|
2069
|
+
return resolved === scope || pathWithinRoot(scope, resolved);
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
function allPathsAllowlisted(absolutePaths, allowlist) {
|
|
2073
|
+
return absolutePaths.length > 0 && absolutePaths.every((entry) => isPathAllowlisted(entry, allowlist));
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
// src/core/capability/broker.ts
|
|
2077
|
+
function hasSandboxRuntime(config) {
|
|
2078
|
+
return config.sandbox.enabled && config.sandbox.runtime !== "none";
|
|
2079
|
+
}
|
|
2080
|
+
function isCapabilityBrokerDemotionActive(config) {
|
|
2081
|
+
return hasSandboxRuntime(config);
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// src/core/capability/paths.ts
|
|
2085
|
+
import path12 from "node:path";
|
|
2086
|
+
function applyPatchTargets(patch) {
|
|
2087
|
+
const targets = [];
|
|
2088
|
+
for (const line of patch.split("\n")) {
|
|
2089
|
+
const match = line.match(/^\*\*\* (?:Add|Delete|Update) File: (.+)$/);
|
|
2090
|
+
if (match?.[1]) {
|
|
2091
|
+
targets.push(match[1]);
|
|
2092
|
+
continue;
|
|
2093
|
+
}
|
|
2094
|
+
const moveMatch = line.match(/^\*\*\* Move to: (.+)$/);
|
|
2095
|
+
if (moveMatch?.[1]) {
|
|
2096
|
+
targets.push(moveMatch[1]);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
return targets;
|
|
2100
|
+
}
|
|
2101
|
+
function extractToolFilePath(payload) {
|
|
2102
|
+
const toolInput = payload.tool_input;
|
|
2103
|
+
if (!toolInput || typeof toolInput !== "object") {
|
|
2104
|
+
return null;
|
|
2105
|
+
}
|
|
2106
|
+
const input = toolInput;
|
|
2107
|
+
for (const key of ["path", "file_path", "target_file", "filePath"]) {
|
|
2108
|
+
if (typeof input[key] === "string") {
|
|
2109
|
+
return input[key];
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
return null;
|
|
2113
|
+
}
|
|
2114
|
+
function extractToolPatch(payload) {
|
|
2115
|
+
const toolInput = payload.tool_input;
|
|
2116
|
+
if (!toolInput || typeof toolInput !== "object") {
|
|
2117
|
+
return null;
|
|
2118
|
+
}
|
|
2119
|
+
const input = toolInput;
|
|
2120
|
+
for (const key of ["patch", "input", "text"]) {
|
|
2121
|
+
if (typeof input[key] === "string" && input[key].trim()) {
|
|
2122
|
+
return input[key];
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
return null;
|
|
1319
2126
|
}
|
|
1320
|
-
function addOutsideRepoPath(paths, token, cwd, repoRoot) {
|
|
2127
|
+
function addOutsideRepoPath(paths, token, cwd, repoRoot, trustedWorkspaceRoots = []) {
|
|
1321
2128
|
const resolved = resolveMutationTarget(token, cwd);
|
|
1322
|
-
if (resolved &&
|
|
2129
|
+
if (resolved && resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolved) === null) {
|
|
1323
2130
|
paths.add(resolved);
|
|
1324
2131
|
}
|
|
1325
2132
|
}
|
|
1326
|
-
function collectOutsideRepoPaths(command, cwd, repoRoot) {
|
|
2133
|
+
function collectOutsideRepoPaths(command, cwd, repoRoot, trustedWorkspaceRoots = []) {
|
|
1327
2134
|
const tokens = tokenizeShell(command);
|
|
1328
2135
|
const redirects = extractRedirectTargets(tokens);
|
|
1329
2136
|
const paths = /* @__PURE__ */ new Set();
|
|
1330
2137
|
for (const token of tokens.slice(1)) {
|
|
1331
|
-
addOutsideRepoPath(paths, token, cwd, repoRoot);
|
|
2138
|
+
addOutsideRepoPath(paths, token, cwd, repoRoot, trustedWorkspaceRoots);
|
|
1332
2139
|
}
|
|
1333
2140
|
for (const redirect of redirects) {
|
|
1334
|
-
addOutsideRepoPath(paths, redirect, cwd, repoRoot);
|
|
2141
|
+
addOutsideRepoPath(paths, redirect, cwd, repoRoot, trustedWorkspaceRoots);
|
|
1335
2142
|
}
|
|
1336
2143
|
return [...paths];
|
|
1337
2144
|
}
|
|
1338
|
-
function collectOutsideRepoPathsFromToolPayload(payload, cwd, repoRoot) {
|
|
2145
|
+
function collectOutsideRepoPathsFromToolPayload(payload, cwd, repoRoot, trustedWorkspaceRoots = []) {
|
|
1339
2146
|
const toolKind = String(payload.tool_name ?? "").trim().toLowerCase();
|
|
1340
2147
|
const paths = /* @__PURE__ */ new Set();
|
|
1341
2148
|
const filePath = extractToolFilePath(payload);
|
|
1342
2149
|
if (filePath) {
|
|
1343
|
-
const resolved =
|
|
1344
|
-
if (
|
|
2150
|
+
const resolved = path12.isAbsolute(filePath) ? filePath : path12.resolve(cwd, filePath);
|
|
2151
|
+
if (resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolved) === null) {
|
|
1345
2152
|
paths.add(resolved);
|
|
1346
2153
|
}
|
|
1347
2154
|
return [...paths];
|
|
@@ -1350,8 +2157,8 @@ function collectOutsideRepoPathsFromToolPayload(payload, cwd, repoRoot) {
|
|
|
1350
2157
|
const patch = extractToolPatch(payload);
|
|
1351
2158
|
if (patch) {
|
|
1352
2159
|
for (const target of applyPatchTargets(patch)) {
|
|
1353
|
-
const resolved =
|
|
1354
|
-
if (
|
|
2160
|
+
const resolved = path12.isAbsolute(target) ? target : path12.resolve(cwd, target);
|
|
2161
|
+
if (resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolved) === null) {
|
|
1355
2162
|
paths.add(resolved);
|
|
1356
2163
|
}
|
|
1357
2164
|
}
|
|
@@ -1374,162 +2181,123 @@ function shouldSkipBrokerApprovedRecord(brokerActive, approvalReason) {
|
|
|
1374
2181
|
return brokerActive && approvalReason !== void 0 && FS_SCOPE_REASONS.has(approvalReason);
|
|
1375
2182
|
}
|
|
1376
2183
|
|
|
1377
|
-
// src/core/
|
|
1378
|
-
import
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
unknownLocalEffect: "deny",
|
|
1387
|
-
unparseableShell: "deny",
|
|
1388
|
-
confidenceThresholds: { allow: 0.9, flag: 0.8 },
|
|
1389
|
-
modelAssist: { enabled: false }
|
|
1390
|
-
},
|
|
1391
|
-
sandbox: { ...DEFAULT_CONFIG_V3.sandbox }
|
|
1392
|
-
},
|
|
1393
|
-
standard: {
|
|
1394
|
-
mode: "enforce"
|
|
1395
|
-
},
|
|
1396
|
-
"audit-first": {
|
|
1397
|
-
mode: "audit",
|
|
1398
|
-
policy: {
|
|
1399
|
-
...DEFAULT_CONFIG_V3.policy,
|
|
1400
|
-
unknownLocalEffect: "deny",
|
|
1401
|
-
unparseableShell: "deny",
|
|
1402
|
-
confidenceThresholds: { allow: 0.88, flag: 0.72 },
|
|
1403
|
-
modelAssist: { enabled: false }
|
|
1404
|
-
},
|
|
1405
|
-
sandbox: { ...DEFAULT_CONFIG_V3.sandbox }
|
|
1406
|
-
},
|
|
1407
|
-
"l1-full-recommended": {
|
|
1408
|
-
mode: "enforce",
|
|
1409
|
-
policy: {
|
|
1410
|
-
...DEFAULT_CONFIG_V3.policy,
|
|
1411
|
-
confidenceThresholds: { ...DEFAULT_CONFIG_V3.policy.confidenceThresholds },
|
|
1412
|
-
modelAssist: { ...DEFAULT_CONFIG_V3.policy.modelAssist }
|
|
1413
|
-
},
|
|
1414
|
-
sandbox: {
|
|
1415
|
-
enabled: true,
|
|
1416
|
-
runtime: "container",
|
|
1417
|
-
denyNetworkByDefault: true
|
|
1418
|
-
},
|
|
1419
|
-
egress: {
|
|
1420
|
-
...DEFAULT_CONFIG_V3.egress,
|
|
1421
|
-
enabled: true,
|
|
1422
|
-
demoteL3External: true
|
|
1423
|
-
},
|
|
1424
|
-
approvalSigning: {
|
|
1425
|
-
required: true
|
|
1426
|
-
},
|
|
1427
|
-
controlPlane: {
|
|
1428
|
-
...DEFAULT_CONFIG_V3.controlPlane,
|
|
1429
|
-
isolation: {
|
|
1430
|
-
mode: "separate-user",
|
|
1431
|
-
verifyAgentWritable: true
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
2184
|
+
// src/core/capability/trusted-workspace-roots.ts
|
|
2185
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
|
|
2186
|
+
import path13 from "node:path";
|
|
2187
|
+
function trustedWorkspaceRootsPath(config, repoLocalStateDir) {
|
|
2188
|
+
return path13.join(belayStateDir(config, repoLocalStateDir), "trusted-workspace-roots.json");
|
|
2189
|
+
}
|
|
2190
|
+
function loadTrustedWorkspaceRootsSync(filePath) {
|
|
2191
|
+
if (!existsSync4(filePath)) {
|
|
2192
|
+
return { version: 1, roots: [] };
|
|
1434
2193
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
const base = CONFIG_PRESETS[preset] ?? CONFIG_PRESETS.standard;
|
|
1438
|
-
return {
|
|
1439
|
-
version: 3,
|
|
1440
|
-
...base,
|
|
1441
|
-
...extra,
|
|
1442
|
-
policy: {
|
|
1443
|
-
...DEFAULT_CONFIG_V3.policy,
|
|
1444
|
-
...base.policy ?? {},
|
|
1445
|
-
...extra.policy
|
|
1446
|
-
},
|
|
1447
|
-
sandbox: {
|
|
1448
|
-
...DEFAULT_CONFIG_V3.sandbox,
|
|
1449
|
-
...base.sandbox ?? {},
|
|
1450
|
-
...extra.sandbox
|
|
1451
|
-
},
|
|
1452
|
-
egress: {
|
|
1453
|
-
...DEFAULT_CONFIG_V3.egress,
|
|
1454
|
-
...base.egress ?? {},
|
|
1455
|
-
...extra.egress
|
|
1456
|
-
},
|
|
1457
|
-
approvalSigning: {
|
|
1458
|
-
...DEFAULT_CONFIG_V3.approvalSigning,
|
|
1459
|
-
...base.approvalSigning ?? {},
|
|
1460
|
-
...extra.approvalSigning
|
|
1461
|
-
},
|
|
1462
|
-
controlPlane: {
|
|
1463
|
-
...DEFAULT_CONFIG_V3.controlPlane,
|
|
1464
|
-
...base.controlPlane ?? {},
|
|
1465
|
-
...extra.controlPlane,
|
|
1466
|
-
isolation: {
|
|
1467
|
-
...DEFAULT_CONFIG_V3.controlPlane.isolation,
|
|
1468
|
-
...base.controlPlane?.isolation ?? {},
|
|
1469
|
-
...extra.controlPlane?.isolation ?? {}
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
};
|
|
2194
|
+
const raw = JSON.parse(readFileSync2(filePath, "utf8"));
|
|
2195
|
+
return { version: 1, roots: sanitizeTrustedWorkspaceRootEntries(raw.roots) };
|
|
1473
2196
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
function teamConfigPath(homedir = () => process.env.HOME ?? process.env.USERPROFILE ?? "") {
|
|
1477
|
-
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
1478
|
-
const base = xdg || path8.join(homedir(), ".config");
|
|
1479
|
-
return path8.join(base, "agent-belay", "team.config.json");
|
|
2197
|
+
function normalizeTrustedWorkspaceRootPath(targetPath) {
|
|
2198
|
+
return canonicalPath(targetPath);
|
|
1480
2199
|
}
|
|
1481
|
-
function
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
controlPlane.enabled = true;
|
|
1485
|
-
}
|
|
1486
|
-
if (builtin.controlPlane.integrity === "hash-pinned" && controlPlane.integrity === "none") {
|
|
1487
|
-
controlPlane.integrity = "hash-pinned";
|
|
2200
|
+
function sanitizeTrustedWorkspaceRootEntries(input) {
|
|
2201
|
+
if (!Array.isArray(input)) {
|
|
2202
|
+
return [];
|
|
1488
2203
|
}
|
|
1489
|
-
return {
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
2204
|
+
return input.flatMap((entry) => {
|
|
2205
|
+
if (!entry || typeof entry !== "object") {
|
|
2206
|
+
return [];
|
|
2207
|
+
}
|
|
2208
|
+
const record = entry;
|
|
2209
|
+
if (typeof record.path !== "string" || !record.path.trim()) {
|
|
2210
|
+
return [];
|
|
2211
|
+
}
|
|
2212
|
+
const approvedAt = typeof record.approvedAt === "string" ? record.approvedAt : (/* @__PURE__ */ new Date(0)).toISOString();
|
|
2213
|
+
const approvalId = typeof record.approvalId === "string" ? record.approvalId : "unknown";
|
|
2214
|
+
const source = record.source === "approval" ? "approval" : void 0;
|
|
2215
|
+
return [
|
|
2216
|
+
{
|
|
2217
|
+
path: path13.resolve(record.path),
|
|
2218
|
+
approvedAt,
|
|
2219
|
+
approvalId,
|
|
2220
|
+
...source ? { source } : {}
|
|
2221
|
+
}
|
|
2222
|
+
];
|
|
2223
|
+
});
|
|
1493
2224
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
2225
|
+
var SYSTEM_HIGH_STAKES_PREFIXES = [
|
|
2226
|
+
"/etc",
|
|
2227
|
+
"/private/etc",
|
|
2228
|
+
"/bin",
|
|
2229
|
+
"/sbin",
|
|
2230
|
+
"/usr",
|
|
2231
|
+
"/opt",
|
|
2232
|
+
"/System",
|
|
2233
|
+
"/Library"
|
|
2234
|
+
];
|
|
2235
|
+
var HOME_HIGH_STAKES_SEGMENTS = [".ssh", ".gnupg", ".aws", ".kube", ".docker", ".config"];
|
|
2236
|
+
function isDirectoryPath(targetPath) {
|
|
2237
|
+
try {
|
|
2238
|
+
return statSync2(targetPath).isDirectory();
|
|
2239
|
+
} catch {
|
|
2240
|
+
return false;
|
|
1497
2241
|
}
|
|
1498
|
-
return { version: 3, ...raw };
|
|
1499
2242
|
}
|
|
1500
|
-
function
|
|
1501
|
-
const
|
|
1502
|
-
|
|
1503
|
-
|
|
2243
|
+
function isBroadTrustedWorkspaceRoot(targetPath) {
|
|
2244
|
+
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
2245
|
+
const root = normalizeTrustedWorkspaceRootPath(targetPath);
|
|
2246
|
+
if (root === normalizeTrustedWorkspaceRootPath(path13.parse(root).root)) {
|
|
2247
|
+
return true;
|
|
1504
2248
|
}
|
|
1505
|
-
|
|
2249
|
+
if (home && normalizeTrustedWorkspaceRootPath(home) === root) {
|
|
2250
|
+
return true;
|
|
2251
|
+
}
|
|
2252
|
+
return false;
|
|
1506
2253
|
}
|
|
1507
|
-
function
|
|
1508
|
-
const
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
rejectTeamLayerJudgeSecrets(
|
|
1514
|
-
teamRaw.judge,
|
|
1515
|
-
"team"
|
|
1516
|
-
);
|
|
1517
|
-
config = mergeConfigLayer(config, asV3Layer(teamRaw));
|
|
1518
|
-
provenance.push({
|
|
1519
|
-
path: params.teamConfigPath ?? teamConfigPath(),
|
|
1520
|
-
source: "team"
|
|
1521
|
-
});
|
|
2254
|
+
function isHighStakesTrustedWorkspaceRoot(targetPath) {
|
|
2255
|
+
const normalized = normalizeTrustedWorkspaceRootPath(targetPath);
|
|
2256
|
+
if (SYSTEM_HIGH_STAKES_PREFIXES.some(
|
|
2257
|
+
(prefix) => normalized === normalizeTrustedWorkspaceRootPath(prefix) || pathWithinRoot(normalizeTrustedWorkspaceRootPath(prefix), normalized)
|
|
2258
|
+
)) {
|
|
2259
|
+
return true;
|
|
1522
2260
|
}
|
|
1523
|
-
|
|
1524
|
-
if (
|
|
1525
|
-
|
|
2261
|
+
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
2262
|
+
if (!home) {
|
|
2263
|
+
return false;
|
|
1526
2264
|
}
|
|
1527
|
-
const
|
|
1528
|
-
if (
|
|
1529
|
-
|
|
1530
|
-
config = protectedConfig;
|
|
2265
|
+
const homeRoot = normalizeTrustedWorkspaceRootPath(home);
|
|
2266
|
+
if (!pathWithinRoot(homeRoot, normalized)) {
|
|
2267
|
+
return false;
|
|
1531
2268
|
}
|
|
1532
|
-
return
|
|
2269
|
+
return HOME_HIGH_STAKES_SEGMENTS.some(
|
|
2270
|
+
(segment) => pathWithinRoot(path13.join(homeRoot, segment), normalized)
|
|
2271
|
+
);
|
|
2272
|
+
}
|
|
2273
|
+
function validateTrustedWorkspaceRootCandidate(params) {
|
|
2274
|
+
const normalizedPath = normalizeTrustedWorkspaceRootPath(params.candidatePath);
|
|
2275
|
+
if (params.requireExistingDirectory !== false && !isDirectoryPath(normalizedPath)) {
|
|
2276
|
+
return { ok: false, normalizedPath, reason: "not_directory" };
|
|
2277
|
+
}
|
|
2278
|
+
if (isBroadTrustedWorkspaceRoot(normalizedPath)) {
|
|
2279
|
+
return { ok: false, normalizedPath, reason: "broad_root" };
|
|
2280
|
+
}
|
|
2281
|
+
if (isHighStakesTrustedWorkspaceRoot(normalizedPath)) {
|
|
2282
|
+
return { ok: false, normalizedPath, reason: "high_stakes" };
|
|
2283
|
+
}
|
|
2284
|
+
if (resolveWorkspaceRootMatch(params.repoRoot, [], normalizedPath) !== null) {
|
|
2285
|
+
return { ok: false, normalizedPath, reason: "inside_repo" };
|
|
2286
|
+
}
|
|
2287
|
+
if (params.requireNonGit !== false && containingGitRoot(normalizedPath)) {
|
|
2288
|
+
return { ok: false, normalizedPath, reason: "inside_git_repo" };
|
|
2289
|
+
}
|
|
2290
|
+
const normalizedControlPlane = params.controlPlaneDir?.trim() ? normalizeTrustedWorkspaceRootPath(params.controlPlaneDir) : null;
|
|
2291
|
+
if (normalizedControlPlane && (pathWithinRoot(normalizedControlPlane, normalizedPath) || pathWithinRoot(normalizedPath, normalizedControlPlane))) {
|
|
2292
|
+
return { ok: false, normalizedPath, reason: "control_plane_overlap" };
|
|
2293
|
+
}
|
|
2294
|
+
if ((params.protectedRoots ?? []).some((root) => {
|
|
2295
|
+
const normalizedRoot = normalizeTrustedWorkspaceRootPath(root);
|
|
2296
|
+
return pathWithinRoot(normalizedRoot, normalizedPath) || pathWithinRoot(normalizedPath, normalizedRoot);
|
|
2297
|
+
})) {
|
|
2298
|
+
return { ok: false, normalizedPath, reason: "protected_root_overlap" };
|
|
2299
|
+
}
|
|
2300
|
+
return { ok: true, normalizedPath };
|
|
1533
2301
|
}
|
|
1534
2302
|
|
|
1535
2303
|
// src/core/gate-contract.ts
|
|
@@ -1574,30 +2342,6 @@ function unnormalizedGateVerdict(params) {
|
|
|
1574
2342
|
};
|
|
1575
2343
|
}
|
|
1576
2344
|
|
|
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);
|
|
1582
|
-
}
|
|
1583
|
-
if (Array.isArray(value)) {
|
|
1584
|
-
return `[${value.map((item) => canonicalStringify(item)).join(",")}]`;
|
|
1585
|
-
}
|
|
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}`);
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
2345
|
// src/core/scrub.ts
|
|
1602
2346
|
var UUID_PATTERN = /\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/gi;
|
|
1603
2347
|
var TIMESTAMP_PATTERN = /\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\b/g;
|
|
@@ -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
|
-
|
|
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
|
|
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/
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
var claudeLayout = {
|
|
1829
|
-
name: "claude",
|
|
1830
|
-
configPath(repoRoot) {
|
|
1831
|
-
return path9.join(repoRoot, ".claude", "belay.config.json");
|
|
1832
|
-
},
|
|
1833
|
-
hooksSettingsPath(repoRoot) {
|
|
1834
|
-
return path9.join(repoRoot, ".claude", "settings.json");
|
|
1835
|
-
},
|
|
1836
|
-
hooksDir(repoRoot) {
|
|
1837
|
-
return path9.join(repoRoot, ".claude", "hooks");
|
|
1838
|
-
},
|
|
1839
|
-
runtimeDir(repoRoot) {
|
|
1840
|
-
return path9.join(repoRoot, ".claude", "belay", "runtime");
|
|
1841
|
-
},
|
|
1842
|
-
repoLocalStateDir(repoRoot) {
|
|
1843
|
-
return path9.join(repoRoot, ".claude", "belay");
|
|
1844
|
-
},
|
|
1845
|
-
defaultAuditLogPath(_repoRoot) {
|
|
1846
|
-
return path9.join(".claude", "belay", "audit.ndjson");
|
|
1847
|
-
},
|
|
1848
|
-
repoRootMarkers: [".git", ".claude"],
|
|
1849
|
-
runnerCommand: runnerCommand2,
|
|
1850
|
-
defaultConfig(repoRoot) {
|
|
1851
|
-
return {
|
|
1852
|
-
...DEFAULT_CONFIG_V4,
|
|
1853
|
-
adapter: "claude",
|
|
1854
|
-
audit: {
|
|
1855
|
-
...DEFAULT_CONFIG_V4.audit,
|
|
1856
|
-
logPath: claudeLayout.defaultAuditLogPath(repoRoot)
|
|
1857
|
-
}
|
|
1858
|
-
};
|
|
1859
|
-
}
|
|
1860
|
-
};
|
|
1861
|
-
|
|
1862
|
-
// src/adapters/layouts/codex.ts
|
|
1863
|
-
import path10 from "node:path";
|
|
1864
|
-
function runnerCommand3(platform, repoRoot, hookName, ...args) {
|
|
1865
|
-
const hooksDir = path10.join(path10.resolve(repoRoot), ".codex", "hooks");
|
|
1866
|
-
return buildRunnerInvocation(platform, hooksDir, repoRoot, hookName, ...args);
|
|
1867
|
-
}
|
|
1868
|
-
var codexLayout = {
|
|
1869
|
-
name: "codex",
|
|
1870
|
-
configPath(repoRoot) {
|
|
1871
|
-
return path10.join(repoRoot, ".codex", "belay.config.json");
|
|
1872
|
-
},
|
|
1873
|
-
// Codex reads lifecycle hooks from `.codex/config.toml` (project layer).
|
|
1874
|
-
hooksSettingsPath(repoRoot) {
|
|
1875
|
-
return path10.join(repoRoot, ".codex", "config.toml");
|
|
1876
|
-
},
|
|
1877
|
-
hooksDir(repoRoot) {
|
|
1878
|
-
return path10.join(repoRoot, ".codex", "hooks");
|
|
1879
|
-
},
|
|
1880
|
-
runtimeDir(repoRoot) {
|
|
1881
|
-
return path10.join(repoRoot, ".codex", "belay", "runtime");
|
|
1882
|
-
},
|
|
1883
|
-
repoLocalStateDir(repoRoot) {
|
|
1884
|
-
return path10.join(repoRoot, ".codex", "belay");
|
|
1885
|
-
},
|
|
1886
|
-
defaultAuditLogPath(_repoRoot) {
|
|
1887
|
-
return path10.join(".codex", "belay", "audit.ndjson");
|
|
1888
|
-
},
|
|
1889
|
-
repoRootMarkers: [".git", ".codex"],
|
|
1890
|
-
runnerCommand: runnerCommand3,
|
|
1891
|
-
defaultConfig(repoRoot) {
|
|
1892
|
-
return {
|
|
1893
|
-
...DEFAULT_CONFIG_V4,
|
|
1894
|
-
adapter: "codex",
|
|
1895
|
-
audit: {
|
|
1896
|
-
...DEFAULT_CONFIG_V4.audit,
|
|
1897
|
-
logPath: codexLayout.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
|
-
|
|
1919
|
-
|
|
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
|
|
2616
|
+
import path14 from "node:path";
|
|
1930
2617
|
var STORE_FILENAME = "credentials.json";
|
|
1931
2618
|
function credentialStorePath(stateDir) {
|
|
1932
|
-
return
|
|
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
|
|
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(
|
|
1975
|
-
const candidate =
|
|
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
|
|
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 :
|
|
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 (
|
|
2820
|
+
if (path16.isAbsolute(expanded)) {
|
|
2134
2821
|
return canonicalPath(expanded);
|
|
2135
2822
|
}
|
|
2136
|
-
return canonicalPath(
|
|
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 (
|
|
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
|
|
2160
|
-
if (
|
|
2846
|
+
const workspaceMatch = resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolvedPath);
|
|
2847
|
+
if (workspaceMatch !== null && matchesSensitivePath(workspaceMatch.relativePath.replaceAll("\\", "/"), sensitivePaths)) {
|
|
2161
2848
|
return true;
|
|
2162
2849
|
}
|
|
2163
|
-
if (
|
|
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
|
|
2380
|
-
|
|
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 (
|
|
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;
|
|
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
|
+
};
|
|
2609
4359
|
}
|
|
2610
|
-
function
|
|
2611
|
-
|
|
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
|
-
|
|
2616
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
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
|
-
|
|
2796
|
-
|
|
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
|
-
|
|
4645
|
+
if (settled) return;
|
|
2800
4646
|
if (code === 0 && stdout.trim()) {
|
|
2801
|
-
|
|
4647
|
+
resolveOnce(stdout.trim());
|
|
2802
4648
|
return;
|
|
2803
4649
|
}
|
|
2804
|
-
|
|
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
|
|
2845
|
-
|
|
2846
|
-
|
|
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
|
|
2849
|
-
if (!
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
3243
|
-
import
|
|
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 =
|
|
3291
|
-
if (!
|
|
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(
|
|
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 =
|
|
3302
|
-
const stop =
|
|
5208
|
+
let current = path19.resolve(startDir);
|
|
5209
|
+
const stop = path19.resolve(stopDir);
|
|
3303
5210
|
while (true) {
|
|
3304
|
-
const packagePath =
|
|
3305
|
-
if (
|
|
5211
|
+
const packagePath = path19.join(current, "package.json");
|
|
5212
|
+
if (existsSync7(packagePath)) {
|
|
3306
5213
|
return packagePath;
|
|
3307
5214
|
}
|
|
3308
|
-
if (current === stop || current ===
|
|
3309
|
-
return
|
|
5215
|
+
if (current === stop || current === path19.dirname(current)) {
|
|
5216
|
+
return existsSync7(packagePath) ? packagePath : null;
|
|
3310
5217
|
}
|
|
3311
|
-
const parent =
|
|
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(
|
|
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 =
|
|
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 =
|
|
3434
|
-
const stop =
|
|
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 =
|
|
3438
|
-
if (
|
|
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 ===
|
|
5350
|
+
if (makefilePath || searchDir === stop || searchDir === path19.dirname(searchDir)) {
|
|
3444
5351
|
break;
|
|
3445
5352
|
}
|
|
3446
|
-
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
|
|
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 =
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
5110
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
7508
|
+
import path22 from "node:path";
|
|
5538
7509
|
function categorizeChange(change, ctx) {
|
|
5539
|
-
const absolutePath = canonicalPath(
|
|
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
|
|
7630
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
5660
7631
|
import { randomUUID } from "node:crypto";
|
|
5661
|
-
import { existsSync as
|
|
5662
|
-
import { copyFile, mkdir as
|
|
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
|
|
7635
|
+
import path23 from "node:path";
|
|
5665
7636
|
function execGit(repoRoot, args) {
|
|
5666
7637
|
return new Promise((resolve, reject) => {
|
|
5667
|
-
const child =
|
|
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 =
|
|
5706
|
-
await
|
|
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 =
|
|
5726
|
-
if (relative.startsWith("..") ||
|
|
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
|
|
7703
|
+
return path23.join(worktreePath, relative);
|
|
5733
7704
|
}
|
|
5734
7705
|
function runShellCommand(command, cwd, timeoutMs) {
|
|
5735
7706
|
return new Promise((resolve) => {
|
|
5736
|
-
const child =
|
|
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
|
|
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(
|
|
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 =
|
|
5817
|
-
if (
|
|
5818
|
-
const backupPath =
|
|
5819
|
-
await
|
|
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 =
|
|
5830
|
-
await
|
|
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
|
|
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) =>
|
|
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
|
|
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 =
|
|
6027
|
-
await
|
|
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
|
|
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
|
|
6053
|
-
await
|
|
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 (
|
|
6063
|
-
teamConfig = JSON.parse(await
|
|
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 =
|
|
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
|
-
|
|
6118
|
-
|
|
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() +
|
|
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:
|
|
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(
|
|
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(
|
|
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}. ${
|
|
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
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
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 gateVerdictToCursorResponse(verdict2) {
|
|
@@ -6450,19 +8888,19 @@ async function appendObservedAudit(ctx, deps, eventName, payload) {
|
|
|
6450
8888
|
}
|
|
6451
8889
|
|
|
6452
8890
|
// src/adapters/shared/repo-root.ts
|
|
6453
|
-
import { existsSync as
|
|
6454
|
-
import
|
|
8891
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
8892
|
+
import path27 from "node:path";
|
|
6455
8893
|
function findRepoRoot(startPath, layout) {
|
|
6456
|
-
let current =
|
|
8894
|
+
let current = path27.resolve(startPath);
|
|
6457
8895
|
while (true) {
|
|
6458
8896
|
for (const marker of layout.repoRootMarkers) {
|
|
6459
|
-
if (
|
|
8897
|
+
if (existsSync11(path27.join(current, marker))) {
|
|
6460
8898
|
return current;
|
|
6461
8899
|
}
|
|
6462
8900
|
}
|
|
6463
|
-
const parent =
|
|
8901
|
+
const parent = path27.dirname(current);
|
|
6464
8902
|
if (parent === current) {
|
|
6465
|
-
return
|
|
8903
|
+
return path27.resolve(startPath);
|
|
6466
8904
|
}
|
|
6467
8905
|
current = parent;
|
|
6468
8906
|
}
|
|
@@ -6510,7 +8948,8 @@ async function runBeforeSubmitPromptHook() {
|
|
|
6510
8948
|
const result = await processApprovalPrompt(ctx, deps, prompt);
|
|
6511
8949
|
jsonResponse({
|
|
6512
8950
|
continue: result.continue,
|
|
6513
|
-
...result.user_message ? { user_message: result.user_message } : {}
|
|
8951
|
+
...result.user_message ? { user_message: result.user_message } : {},
|
|
8952
|
+
...result.replay ? { replay: result.replay } : {}
|
|
6514
8953
|
});
|
|
6515
8954
|
} catch {
|
|
6516
8955
|
jsonResponse({
|