@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
|
|
@@ -904,12 +1059,216 @@ var codexLayout = {
|
|
|
904
1059
|
|
|
905
1060
|
// src/adapters/shared/gate-runtime.ts
|
|
906
1061
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
907
|
-
import { existsSync as
|
|
908
|
-
import { mkdir as
|
|
909
|
-
import
|
|
1062
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
1063
|
+
import { mkdir as mkdir6, readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
|
|
1064
|
+
import path26 from "node:path";
|
|
1065
|
+
|
|
1066
|
+
// src/core/approval-replay.ts
|
|
1067
|
+
import path5 from "node:path";
|
|
1068
|
+
|
|
1069
|
+
// src/core/fingerprint.ts
|
|
1070
|
+
import { createHash } from "node:crypto";
|
|
1071
|
+
function canonicalStringify(value) {
|
|
1072
|
+
if (value === null || typeof value !== "object") {
|
|
1073
|
+
return JSON.stringify(value);
|
|
1074
|
+
}
|
|
1075
|
+
if (Array.isArray(value)) {
|
|
1076
|
+
return `[${value.map((item) => canonicalStringify(item)).join(",")}]`;
|
|
1077
|
+
}
|
|
1078
|
+
const entries = Object.entries(value).sort(
|
|
1079
|
+
([left], [right]) => left.localeCompare(right)
|
|
1080
|
+
);
|
|
1081
|
+
return `{${entries.map(([key, child]) => `${JSON.stringify(key)}:${canonicalStringify(child)}`).join(",")}}`;
|
|
1082
|
+
}
|
|
1083
|
+
function hashValue(value) {
|
|
1084
|
+
return createHash("sha256").update(value).digest("hex");
|
|
1085
|
+
}
|
|
1086
|
+
function subagentFingerprint(kind, scrubbed, repoRoot) {
|
|
1087
|
+
return hashValue(`subagent:${kind}:${canonicalStringify(scrubbed)}:${repoRoot}`);
|
|
1088
|
+
}
|
|
1089
|
+
function toolFingerprint(toolName, scrubbed, repoRoot) {
|
|
1090
|
+
return hashValue(`tool:${toolName}:${canonicalStringify(scrubbed)}:${repoRoot}`);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/core/approval-replay-cli.ts
|
|
1094
|
+
import { spawn } from "node:child_process";
|
|
1095
|
+
import path4 from "node:path";
|
|
1096
|
+
var DEFAULT_CLI_REPLAY_TIMEOUT_MS = 3e5;
|
|
1097
|
+
async function replayShellCommand(command, cwd, timeoutMs = DEFAULT_CLI_REPLAY_TIMEOUT_MS) {
|
|
1098
|
+
const resolvedCwd = path4.resolve(cwd);
|
|
1099
|
+
return new Promise((resolve, reject) => {
|
|
1100
|
+
const child = spawn(command, {
|
|
1101
|
+
cwd: resolvedCwd,
|
|
1102
|
+
shell: true,
|
|
1103
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1104
|
+
env: { ...process.env }
|
|
1105
|
+
});
|
|
1106
|
+
const stdout = [];
|
|
1107
|
+
const stderr = [];
|
|
1108
|
+
let timedOut = false;
|
|
1109
|
+
const timer = setTimeout(() => {
|
|
1110
|
+
timedOut = true;
|
|
1111
|
+
child.kill("SIGTERM");
|
|
1112
|
+
}, timeoutMs);
|
|
1113
|
+
child.stdout?.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
|
|
1114
|
+
child.stderr?.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
|
|
1115
|
+
child.on("error", (error) => {
|
|
1116
|
+
clearTimeout(timer);
|
|
1117
|
+
reject(error);
|
|
1118
|
+
});
|
|
1119
|
+
child.on("close", (code) => {
|
|
1120
|
+
clearTimeout(timer);
|
|
1121
|
+
resolve({
|
|
1122
|
+
exitCode: timedOut ? 124 : code ?? 1,
|
|
1123
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
1124
|
+
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
1125
|
+
...timedOut ? { timedOut: true } : {}
|
|
1126
|
+
});
|
|
1127
|
+
});
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// src/core/approval-replay.ts
|
|
1132
|
+
var MAX_PAYLOAD_JSON_BYTES = 16384;
|
|
1133
|
+
function replayPayloadHash(kind, payload, repoRoot) {
|
|
1134
|
+
if (!payload || Object.keys(payload).length === 0) {
|
|
1135
|
+
return void 0;
|
|
1136
|
+
}
|
|
1137
|
+
const json = canonicalStringify(payload);
|
|
1138
|
+
return hashValue(`payload:${kind}:${json}:${repoRoot}`);
|
|
1139
|
+
}
|
|
1140
|
+
var DEFAULT_REPLAY_CAPABILITIES = {
|
|
1141
|
+
shell: true,
|
|
1142
|
+
tool: false,
|
|
1143
|
+
subagent: false
|
|
1144
|
+
};
|
|
1145
|
+
var ADAPTER_REPLAY_CAPABLE = {
|
|
1146
|
+
cursor: { ...DEFAULT_REPLAY_CAPABILITIES },
|
|
1147
|
+
claude: { ...DEFAULT_REPLAY_CAPABILITIES },
|
|
1148
|
+
codex: { ...DEFAULT_REPLAY_CAPABILITIES }
|
|
1149
|
+
};
|
|
1150
|
+
function getExecutionLeaseMs(config) {
|
|
1151
|
+
const ms = config.approval?.executionLeaseMs;
|
|
1152
|
+
return typeof ms === "number" && ms > 0 ? ms : 6e4;
|
|
1153
|
+
}
|
|
1154
|
+
function approvalFlow(config) {
|
|
1155
|
+
return config.approval?.flow === "two_step" ? "two_step" : "one_step";
|
|
1156
|
+
}
|
|
1157
|
+
function buildReplayEnvelopeFields(input) {
|
|
1158
|
+
const fields = {};
|
|
1159
|
+
const cmd = input.command ?? input.input ?? "";
|
|
1160
|
+
if (cmd) {
|
|
1161
|
+
fields.input = cmd;
|
|
1162
|
+
fields.inputKind = input.inputKind ?? input.kind;
|
|
1163
|
+
}
|
|
1164
|
+
if (input.cwd) {
|
|
1165
|
+
fields.cwd = path5.resolve(input.cwd);
|
|
1166
|
+
}
|
|
1167
|
+
if (input.toolName) {
|
|
1168
|
+
fields.toolName = input.toolName;
|
|
1169
|
+
}
|
|
1170
|
+
if (input.payload && Object.keys(input.payload).length > 0) {
|
|
1171
|
+
const json = canonicalStringify(input.payload);
|
|
1172
|
+
if (Buffer.byteLength(json, "utf8") <= MAX_PAYLOAD_JSON_BYTES) {
|
|
1173
|
+
fields.payloadJson = json;
|
|
1174
|
+
}
|
|
1175
|
+
fields.payloadHash = replayPayloadHash(input.kind, input.payload, input.repoRoot);
|
|
1176
|
+
}
|
|
1177
|
+
return fields;
|
|
1178
|
+
}
|
|
1179
|
+
function hasReplayEnvelope(approval) {
|
|
1180
|
+
return Boolean(approval.cwd || approval.toolName || approval.payloadHash);
|
|
1181
|
+
}
|
|
1182
|
+
function validateReplayEnvelope(approval, action) {
|
|
1183
|
+
if (!hasReplayEnvelope(approval)) {
|
|
1184
|
+
return true;
|
|
1185
|
+
}
|
|
1186
|
+
if (approval.kind !== action.kind) {
|
|
1187
|
+
return false;
|
|
1188
|
+
}
|
|
1189
|
+
if (approval.fingerprint !== action.fingerprint) {
|
|
1190
|
+
return false;
|
|
1191
|
+
}
|
|
1192
|
+
if (approval.repoRoot !== action.repoRoot) {
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
1195
|
+
if (approval.cwd && path5.resolve(approval.cwd) !== path5.resolve(action.cwd ?? "")) {
|
|
1196
|
+
return false;
|
|
1197
|
+
}
|
|
1198
|
+
if (approval.toolName && approval.toolName !== action.toolName) {
|
|
1199
|
+
return false;
|
|
1200
|
+
}
|
|
1201
|
+
if (approval.payloadHash) {
|
|
1202
|
+
const hash = replayPayloadHash(action.kind, action.payload, action.repoRoot);
|
|
1203
|
+
if (!hash || approval.payloadHash !== hash) {
|
|
1204
|
+
return false;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
return true;
|
|
1208
|
+
}
|
|
1209
|
+
function canAutoReplay(config, kind, adapter) {
|
|
1210
|
+
if (config.approvalSigning?.required) {
|
|
1211
|
+
return false;
|
|
1212
|
+
}
|
|
1213
|
+
if (approvalFlow(config) !== "one_step") {
|
|
1214
|
+
return false;
|
|
1215
|
+
}
|
|
1216
|
+
if (kind !== "shell" && kind !== "tool" && kind !== "subagent") {
|
|
1217
|
+
return false;
|
|
1218
|
+
}
|
|
1219
|
+
const scopes = config.approval?.autoReplayScopes;
|
|
1220
|
+
if (kind === "shell" && scopes?.shell === false) {
|
|
1221
|
+
return false;
|
|
1222
|
+
}
|
|
1223
|
+
if (kind === "tool" && scopes?.tool !== true) {
|
|
1224
|
+
return false;
|
|
1225
|
+
}
|
|
1226
|
+
if (kind === "subagent" && scopes?.subagent !== true) {
|
|
1227
|
+
return false;
|
|
1228
|
+
}
|
|
1229
|
+
if (adapter) {
|
|
1230
|
+
return ADAPTER_REPLAY_CAPABLE[adapter][kind] === true;
|
|
1231
|
+
}
|
|
1232
|
+
return kind === "shell" && scopes?.shell !== false;
|
|
1233
|
+
}
|
|
1234
|
+
function buildRetryInstructionForConfig(config, tokenPrefix, approvalId) {
|
|
1235
|
+
if (approvalFlow(config) === "one_step") {
|
|
1236
|
+
return `To allow this action once, send ${tokenPrefix} ${approvalId}. After approval, retry the same action unchanged.`;
|
|
1237
|
+
}
|
|
1238
|
+
return `To allow the next matching action once, send ${tokenPrefix} ${approvalId} and then retry the original action unchanged.`;
|
|
1239
|
+
}
|
|
1240
|
+
function buildApprovalRecordedMessage(config, approval, adapter) {
|
|
1241
|
+
if (!canAutoReplay(config, approval.kind, adapter)) {
|
|
1242
|
+
return `Belay approval recorded for ${approval.approvalId}. Retry the original action once before it expires.`;
|
|
1243
|
+
}
|
|
1244
|
+
if (approval.kind === "shell" && approval.input) {
|
|
1245
|
+
return `Belay approval recorded for ${approval.approvalId}. Retry this shell command unchanged: ${approval.input}`;
|
|
1246
|
+
}
|
|
1247
|
+
return `Belay approval recorded for ${approval.approvalId}. Retry the original action once before it expires.`;
|
|
1248
|
+
}
|
|
1249
|
+
function buildReplayHint(config, approval, adapter) {
|
|
1250
|
+
if (approvalFlow(config) !== "one_step") {
|
|
1251
|
+
return null;
|
|
1252
|
+
}
|
|
1253
|
+
const input = approval.input ?? approval.summary;
|
|
1254
|
+
if (!input) {
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
const autoReplay = canAutoReplay(config, approval.kind, adapter);
|
|
1258
|
+
return {
|
|
1259
|
+
kind: approval.kind,
|
|
1260
|
+
input,
|
|
1261
|
+
cwd: approval.cwd,
|
|
1262
|
+
toolName: approval.toolName,
|
|
1263
|
+
fingerprint: approval.fingerprint,
|
|
1264
|
+
approvalId: approval.approvalId,
|
|
1265
|
+
autoReplay,
|
|
1266
|
+
fallbackToTwoStep: !autoReplay
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
910
1269
|
|
|
911
1270
|
// src/core/approval.ts
|
|
912
|
-
var APPROVAL_EXECUTION_LEASE_MS =
|
|
1271
|
+
var APPROVAL_EXECUTION_LEASE_MS = DEFAULT_APPROVAL_CONFIG.executionLeaseMs;
|
|
913
1272
|
function nowIso() {
|
|
914
1273
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
915
1274
|
}
|
|
@@ -946,8 +1305,34 @@ function approvalCommandMatch(prompt, tokenPrefix) {
|
|
|
946
1305
|
}
|
|
947
1306
|
return null;
|
|
948
1307
|
}
|
|
949
|
-
function
|
|
950
|
-
|
|
1308
|
+
function createApprovalRecordWithEnvelope(params) {
|
|
1309
|
+
const envelope = buildReplayEnvelopeFields({
|
|
1310
|
+
kind: params.kind,
|
|
1311
|
+
command: params.approvalInput?.input,
|
|
1312
|
+
input: params.approvalInput?.input,
|
|
1313
|
+
inputKind: params.approvalInput?.inputKind,
|
|
1314
|
+
cwd: params.approvalInput?.cwd,
|
|
1315
|
+
toolName: params.approvalInput?.toolName,
|
|
1316
|
+
payload: params.approvalInput?.payload,
|
|
1317
|
+
fingerprint: params.fingerprint,
|
|
1318
|
+
repoRoot: params.repoRoot
|
|
1319
|
+
});
|
|
1320
|
+
return createApprovalRecord({
|
|
1321
|
+
kind: params.kind,
|
|
1322
|
+
fingerprint: params.fingerprint,
|
|
1323
|
+
repoRoot: params.repoRoot,
|
|
1324
|
+
reason: params.reason,
|
|
1325
|
+
summary: params.summary,
|
|
1326
|
+
approvalTtlMinutes: params.approvalTtlMinutes,
|
|
1327
|
+
approvalId: params.approvalId,
|
|
1328
|
+
input: envelope.input ?? params.approvalInput?.input,
|
|
1329
|
+
inputKind: envelope.inputKind ?? params.approvalInput?.inputKind,
|
|
1330
|
+
cwd: envelope.cwd,
|
|
1331
|
+
toolName: envelope.toolName ?? params.approvalInput?.toolName,
|
|
1332
|
+
payloadHash: envelope.payloadHash,
|
|
1333
|
+
payloadJson: envelope.payloadJson,
|
|
1334
|
+
scopeHint: params.scopeHint
|
|
1335
|
+
});
|
|
951
1336
|
}
|
|
952
1337
|
function createApprovalRecord(params) {
|
|
953
1338
|
const createdAt = nowIso();
|
|
@@ -966,221 +1351,514 @@ function createApprovalRecord(params) {
|
|
|
966
1351
|
record.input = params.input;
|
|
967
1352
|
record.inputKind = params.inputKind ?? params.kind;
|
|
968
1353
|
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
// src/core/approval-token.ts
|
|
973
|
-
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
974
|
-
import { existsSync } from "node:fs";
|
|
975
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
976
|
-
import path4 from "node:path";
|
|
977
|
-
function base64UrlEncode(value) {
|
|
978
|
-
return Buffer.from(value, "utf8").toString("base64url");
|
|
979
|
-
}
|
|
980
|
-
function base64UrlDecode(value) {
|
|
981
|
-
return Buffer.from(value, "base64url").toString("utf8");
|
|
982
|
-
}
|
|
983
|
-
function approvalSigningKeyPath(controlPlaneDir = defaultControlPlaneDir()) {
|
|
984
|
-
return path4.join(controlPlaneDir, "approval-signing.key");
|
|
985
|
-
}
|
|
986
|
-
async function loadOrCreateApprovalSigningKey(controlPlaneDir = defaultControlPlaneDir()) {
|
|
987
|
-
const keyPath = approvalSigningKeyPath(controlPlaneDir);
|
|
988
|
-
if (existsSync(keyPath)) {
|
|
989
|
-
return readFile(keyPath);
|
|
1354
|
+
if (params.cwd) {
|
|
1355
|
+
record.cwd = params.cwd;
|
|
990
1356
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
await writeFile(keyPath, key, { mode: 384 });
|
|
994
|
-
return key;
|
|
995
|
-
}
|
|
996
|
-
function signPayload(payload, key) {
|
|
997
|
-
const body = base64UrlEncode(JSON.stringify(payload));
|
|
998
|
-
const signature = createHmac("sha256", key).update(body).digest("base64url");
|
|
999
|
-
return `${body}.${signature}`;
|
|
1000
|
-
}
|
|
1001
|
-
async function issueApprovalToken(payload, controlPlaneDir = defaultControlPlaneDir()) {
|
|
1002
|
-
const key = await loadOrCreateApprovalSigningKey(controlPlaneDir);
|
|
1003
|
-
return signPayload(payload, key);
|
|
1004
|
-
}
|
|
1005
|
-
async function verifyApprovalToken(token, controlPlaneDir = defaultControlPlaneDir()) {
|
|
1006
|
-
const [body, signature] = token.split(".");
|
|
1007
|
-
if (!body || !signature) {
|
|
1008
|
-
return null;
|
|
1357
|
+
if (params.toolName) {
|
|
1358
|
+
record.toolName = params.toolName;
|
|
1009
1359
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
return null;
|
|
1360
|
+
if (params.payloadHash) {
|
|
1361
|
+
record.payloadHash = params.payloadHash;
|
|
1013
1362
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
const actualBuffer = Buffer.from(signature);
|
|
1017
|
-
const expectedBuffer = Buffer.from(expected);
|
|
1018
|
-
if (actualBuffer.length !== expectedBuffer.length || !timingSafeEqual(actualBuffer, expectedBuffer)) {
|
|
1019
|
-
return null;
|
|
1363
|
+
if (params.payloadJson) {
|
|
1364
|
+
record.payloadJson = params.payloadJson;
|
|
1020
1365
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
if (Date.parse(payload.expiresAt) <= Date.now()) {
|
|
1024
|
-
return null;
|
|
1025
|
-
}
|
|
1026
|
-
return payload;
|
|
1027
|
-
} catch {
|
|
1028
|
-
return null;
|
|
1366
|
+
if (params.scopeHint) {
|
|
1367
|
+
record.scopeHint = params.scopeHint;
|
|
1029
1368
|
}
|
|
1369
|
+
return record;
|
|
1030
1370
|
}
|
|
1031
1371
|
|
|
1032
|
-
// src/
|
|
1033
|
-
async function recordApproval(params) {
|
|
1034
|
-
const { approvalId, config, store, token, requireSignedToken = false } = params;
|
|
1035
|
-
const pending = await store.loadPending();
|
|
1036
|
-
pending.state = compactApprovals(pending.state);
|
|
1037
|
-
const index = pending.state.approvals.findIndex((approval2) => approval2.approvalId === approvalId);
|
|
1038
|
-
if (index === -1) {
|
|
1039
|
-
await store.writePending(pending.filePath, pending.state);
|
|
1040
|
-
return { ok: false, message: "Belay approval not found or expired." };
|
|
1041
|
-
}
|
|
1042
|
-
const [approval] = pending.state.approvals.slice(index, index + 1);
|
|
1043
|
-
if (requireSignedToken) {
|
|
1044
|
-
if (!token) {
|
|
1045
|
-
return { ok: false, message: "Signed approval token required for out-of-band approval." };
|
|
1046
|
-
}
|
|
1047
|
-
const controlPlaneDir = configuredControlPlaneDir(config);
|
|
1048
|
-
const verified = await verifyApprovalToken(token, controlPlaneDir);
|
|
1049
|
-
if (!verified || verified.approvalId !== approvalId) {
|
|
1050
|
-
return { ok: false, message: "Invalid or expired signed approval token." };
|
|
1051
|
-
}
|
|
1052
|
-
if (verified.fingerprint !== approval.fingerprint || verified.repoRoot !== approval.repoRoot) {
|
|
1053
|
-
return { ok: false, message: "Signed approval token does not match the pending approval." };
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
pending.state.approvals.splice(index, 1);
|
|
1057
|
-
await store.writePending(pending.filePath, pending.state);
|
|
1058
|
-
const approved = await store.loadApproved();
|
|
1059
|
-
approved.state = compactApprovals(approved.state);
|
|
1060
|
-
approved.state.approvals.push({
|
|
1061
|
-
...approval,
|
|
1062
|
-
approvedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1063
|
-
});
|
|
1064
|
-
await store.writeApproved(approved.filePath, approved.state);
|
|
1065
|
-
return {
|
|
1066
|
-
ok: true,
|
|
1067
|
-
message: `Belay approval recorded for ${approvalId}. Retry the original action once before it expires.`,
|
|
1068
|
-
approval
|
|
1069
|
-
};
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
// src/core/capability/allowlist.ts
|
|
1073
|
-
import { existsSync as existsSync3, readFileSync } from "node:fs";
|
|
1372
|
+
// src/adapters/layouts/claude.ts
|
|
1074
1373
|
import path6 from "node:path";
|
|
1374
|
+
function runnerCommand2(platform, repoRoot, hookName, ...args) {
|
|
1375
|
+
const hooksDir = path6.join(path6.resolve(repoRoot), ".claude", "hooks");
|
|
1376
|
+
return buildRunnerInvocation(platform, hooksDir, repoRoot, hookName, ...args);
|
|
1377
|
+
}
|
|
1378
|
+
var claudeLayout = {
|
|
1379
|
+
name: "claude",
|
|
1380
|
+
configPath(repoRoot) {
|
|
1381
|
+
return path6.join(repoRoot, ".claude", "belay.config.json");
|
|
1382
|
+
},
|
|
1383
|
+
hooksSettingsPath(repoRoot) {
|
|
1384
|
+
return path6.join(repoRoot, ".claude", "settings.json");
|
|
1385
|
+
},
|
|
1386
|
+
hooksDir(repoRoot) {
|
|
1387
|
+
return path6.join(repoRoot, ".claude", "hooks");
|
|
1388
|
+
},
|
|
1389
|
+
runtimeDir(repoRoot) {
|
|
1390
|
+
return path6.join(repoRoot, ".claude", "belay", "runtime");
|
|
1391
|
+
},
|
|
1392
|
+
repoLocalStateDir(repoRoot) {
|
|
1393
|
+
return path6.join(repoRoot, ".claude", "belay");
|
|
1394
|
+
},
|
|
1395
|
+
defaultAuditLogPath(_repoRoot) {
|
|
1396
|
+
return path6.join(".claude", "belay", "audit.ndjson");
|
|
1397
|
+
},
|
|
1398
|
+
repoRootMarkers: [".git", ".claude"],
|
|
1399
|
+
runnerCommand: runnerCommand2,
|
|
1400
|
+
defaultConfig(repoRoot) {
|
|
1401
|
+
return {
|
|
1402
|
+
...DEFAULT_CONFIG_V4,
|
|
1403
|
+
adapter: "claude",
|
|
1404
|
+
audit: {
|
|
1405
|
+
...DEFAULT_CONFIG_V4.audit,
|
|
1406
|
+
logPath: claudeLayout.defaultAuditLogPath(repoRoot)
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
};
|
|
1075
1411
|
|
|
1076
|
-
// src/
|
|
1077
|
-
import
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1412
|
+
// src/adapters/layouts/cursor.ts
|
|
1413
|
+
import path7 from "node:path";
|
|
1414
|
+
function runnerCommand3(platform, repoRoot, hookName, ...args) {
|
|
1415
|
+
const hooksDir = path7.join(path7.resolve(repoRoot), ".cursor", "hooks");
|
|
1416
|
+
return buildRunnerInvocation(platform, hooksDir, repoRoot, hookName, ...args);
|
|
1417
|
+
}
|
|
1418
|
+
var cursorLayout = {
|
|
1419
|
+
name: "cursor",
|
|
1420
|
+
configPath(repoRoot) {
|
|
1421
|
+
return path7.join(repoRoot, ".cursor", "belay.config.json");
|
|
1422
|
+
},
|
|
1423
|
+
hooksSettingsPath(repoRoot) {
|
|
1424
|
+
return path7.join(repoRoot, ".cursor", "hooks.json");
|
|
1425
|
+
},
|
|
1426
|
+
hooksDir(repoRoot) {
|
|
1427
|
+
return path7.join(repoRoot, ".cursor", "hooks");
|
|
1428
|
+
},
|
|
1429
|
+
runtimeDir(repoRoot) {
|
|
1430
|
+
return path7.join(repoRoot, ".cursor", "belay", "runtime");
|
|
1431
|
+
},
|
|
1432
|
+
repoLocalStateDir(repoRoot) {
|
|
1433
|
+
return path7.join(repoRoot, ".cursor", "belay");
|
|
1434
|
+
},
|
|
1435
|
+
defaultAuditLogPath(_repoRoot) {
|
|
1436
|
+
return path7.join(".cursor", "belay", "audit.ndjson");
|
|
1437
|
+
},
|
|
1438
|
+
repoRootMarkers: [".git", ".cursor"],
|
|
1439
|
+
runnerCommand: runnerCommand3,
|
|
1440
|
+
defaultConfig(repoRoot) {
|
|
1441
|
+
return {
|
|
1442
|
+
...DEFAULT_CONFIG_V4,
|
|
1443
|
+
adapter: "cursor",
|
|
1444
|
+
audit: {
|
|
1445
|
+
...DEFAULT_CONFIG_V4.audit,
|
|
1446
|
+
logPath: cursorLayout.defaultAuditLogPath(repoRoot)
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1083
1449
|
}
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
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];
|
|
1460
|
+
}
|
|
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
|
+
}
|
|
1100
1518
|
}
|
|
1101
1519
|
}
|
|
1102
|
-
|
|
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
|
+
};
|
|
1103
1558
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
const
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
}
|
|
1111
|
-
return !relativePath.startsWith("..") && !path5.isAbsolute(relativePath);
|
|
1559
|
+
|
|
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");
|
|
1112
1565
|
}
|
|
1113
|
-
function
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
if (relativePath === "") {
|
|
1118
|
-
return ".";
|
|
1566
|
+
function applyProtectedLayer(config, builtin) {
|
|
1567
|
+
const controlPlane = { ...config.controlPlane };
|
|
1568
|
+
if (builtin.controlPlane.enabled && controlPlane.enabled === false) {
|
|
1569
|
+
controlPlane.enabled = true;
|
|
1119
1570
|
}
|
|
1120
|
-
if (
|
|
1121
|
-
|
|
1571
|
+
if (builtin.controlPlane.integrity === "hash-pinned" && controlPlane.integrity === "none") {
|
|
1572
|
+
controlPlane.integrity = "hash-pinned";
|
|
1122
1573
|
}
|
|
1123
|
-
return
|
|
1574
|
+
return {
|
|
1575
|
+
...config,
|
|
1576
|
+
controlPlane
|
|
1577
|
+
};
|
|
1124
1578
|
}
|
|
1125
|
-
function
|
|
1126
|
-
if (!
|
|
1127
|
-
return
|
|
1579
|
+
function asV3Layer(raw) {
|
|
1580
|
+
if (!raw || typeof raw !== "object") {
|
|
1581
|
+
return { version: 3 };
|
|
1128
1582
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1583
|
+
return { version: 3, ...raw };
|
|
1584
|
+
}
|
|
1585
|
+
function mergeConfigLayer(base, layer) {
|
|
1586
|
+
const merged = mergeConfig(layer, base);
|
|
1587
|
+
if (!layer.policy) {
|
|
1588
|
+
return { ...merged, policy: base.policy };
|
|
1131
1589
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1590
|
+
return merged;
|
|
1591
|
+
}
|
|
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
|
+
});
|
|
1134
1607
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1608
|
+
config = mergeConfigLayer(config, asV3Layer(params.repoConfig));
|
|
1609
|
+
if (params.repoConfigPath) {
|
|
1610
|
+
provenance.push({ path: params.repoConfigPath, source: "repo" });
|
|
1137
1611
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
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;
|
|
1140
1616
|
}
|
|
1141
|
-
return
|
|
1617
|
+
return { config, provenance };
|
|
1142
1618
|
}
|
|
1143
1619
|
|
|
1144
|
-
// src/
|
|
1145
|
-
function
|
|
1146
|
-
|
|
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";
|
|
1147
1629
|
}
|
|
1148
|
-
function
|
|
1149
|
-
|
|
1150
|
-
|
|
1630
|
+
function repoLocalStateDirFor(repoRoot, config) {
|
|
1631
|
+
return getAdapterLayout(resolveAdapterName(config)).repoLocalStateDir(repoRoot);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
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);
|
|
1151
1652
|
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
};
|
|
1653
|
+
await mkdir(controlPlaneDir, { recursive: true });
|
|
1654
|
+
const key = randomBytes(32);
|
|
1655
|
+
await writeFile(keyPath, key, { mode: 384 });
|
|
1656
|
+
return key;
|
|
1157
1657
|
}
|
|
1158
|
-
function
|
|
1159
|
-
|
|
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}`;
|
|
1160
1662
|
}
|
|
1161
|
-
function
|
|
1162
|
-
const
|
|
1163
|
-
return
|
|
1164
|
-
|
|
1165
|
-
|
|
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()
|
|
1166
1725
|
});
|
|
1726
|
+
await store.writeApproved(approved.filePath, approved.state);
|
|
1727
|
+
return {
|
|
1728
|
+
ok: true,
|
|
1729
|
+
message: buildApprovalRecordedMessage(config, approval, adapter),
|
|
1730
|
+
approval
|
|
1731
|
+
};
|
|
1167
1732
|
}
|
|
1168
|
-
function
|
|
1169
|
-
|
|
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
|
+
};
|
|
1170
1752
|
}
|
|
1171
1753
|
|
|
1172
|
-
// src/core/
|
|
1173
|
-
|
|
1174
|
-
|
|
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");
|
|
1175
1758
|
}
|
|
1176
|
-
function
|
|
1177
|
-
|
|
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
|
+
};
|
|
1178
1791
|
}
|
|
1179
1792
|
|
|
1180
|
-
// src/core/capability/
|
|
1181
|
-
import
|
|
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";
|
|
1182
1800
|
|
|
1183
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
|
+
}
|
|
1184
1862
|
function tokenizeShell(input) {
|
|
1185
1863
|
const tokens = [];
|
|
1186
1864
|
let buffer = "";
|
|
@@ -1194,7 +1872,6 @@ function tokenizeShell(input) {
|
|
|
1194
1872
|
};
|
|
1195
1873
|
for (let index = 0; index < input.length; index += 1) {
|
|
1196
1874
|
const char = input[index];
|
|
1197
|
-
const next = input[index + 1] ?? "";
|
|
1198
1875
|
if (escaping) {
|
|
1199
1876
|
buffer += char;
|
|
1200
1877
|
escaping = false;
|
|
@@ -1216,27 +1893,11 @@ function tokenizeShell(input) {
|
|
|
1216
1893
|
quote = char;
|
|
1217
1894
|
continue;
|
|
1218
1895
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
tokens.push("&&");
|
|
1222
|
-
index += 1;
|
|
1223
|
-
continue;
|
|
1224
|
-
}
|
|
1225
|
-
if (char === "|" && next === "|") {
|
|
1226
|
-
flush();
|
|
1227
|
-
tokens.push("||");
|
|
1228
|
-
index += 1;
|
|
1229
|
-
continue;
|
|
1230
|
-
}
|
|
1231
|
-
if (char === ">" && next === ">") {
|
|
1232
|
-
flush();
|
|
1233
|
-
tokens.push(">>");
|
|
1234
|
-
index += 1;
|
|
1235
|
-
continue;
|
|
1236
|
-
}
|
|
1237
|
-
if (char === "|" || char === ";" || char === ">" || char === "<") {
|
|
1896
|
+
const operator = readShellOperator(input, index);
|
|
1897
|
+
if (operator) {
|
|
1238
1898
|
flush();
|
|
1239
|
-
tokens.push(
|
|
1899
|
+
tokens.push(operator.token);
|
|
1900
|
+
index += operator.length - 1;
|
|
1240
1901
|
continue;
|
|
1241
1902
|
}
|
|
1242
1903
|
if (char === "\n" || char === "\r") {
|
|
@@ -1266,7 +1927,10 @@ function extractRedirectTargets(tokens) {
|
|
|
1266
1927
|
const targets = [];
|
|
1267
1928
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
1268
1929
|
const token = tokens[index];
|
|
1269
|
-
if (token
|
|
1930
|
+
if (isFdDuplication(token)) {
|
|
1931
|
+
continue;
|
|
1932
|
+
}
|
|
1933
|
+
if (isRedirectOperator(token)) {
|
|
1270
1934
|
const next = tokens[index + 1];
|
|
1271
1935
|
if (next) {
|
|
1272
1936
|
targets.push(next);
|
|
@@ -1276,73 +1940,215 @@ function extractRedirectTargets(tokens) {
|
|
|
1276
1940
|
return targets;
|
|
1277
1941
|
}
|
|
1278
1942
|
|
|
1279
|
-
// src/core/
|
|
1280
|
-
function
|
|
1281
|
-
const
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
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) {
|
|
1286
1955
|
continue;
|
|
1287
1956
|
}
|
|
1288
|
-
const
|
|
1289
|
-
if (
|
|
1290
|
-
|
|
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));
|
|
1291
1965
|
}
|
|
1292
1966
|
}
|
|
1293
|
-
return
|
|
1967
|
+
return current;
|
|
1294
1968
|
}
|
|
1295
|
-
function
|
|
1296
|
-
const
|
|
1297
|
-
|
|
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)) {
|
|
1298
1986
|
return null;
|
|
1299
1987
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
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 };
|
|
1304
2004
|
}
|
|
1305
2005
|
}
|
|
1306
2006
|
return null;
|
|
1307
2007
|
}
|
|
1308
|
-
function
|
|
1309
|
-
|
|
1310
|
-
if (!toolInput || typeof toolInput !== "object") {
|
|
2008
|
+
function resolveMutationTarget(token, cwd) {
|
|
2009
|
+
if (!token || token === "--" || token.startsWith("-")) {
|
|
1311
2010
|
return null;
|
|
1312
2011
|
}
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
if (typeof input[key] === "string" && input[key].trim()) {
|
|
1316
|
-
return input[key];
|
|
1317
|
-
}
|
|
2012
|
+
if (isRedirectOperator(token) || isFdDuplication(token) || token === "&&" || token === "||" || token === "|" || token === "|&" || token === ";" || token === "&") {
|
|
2013
|
+
return null;
|
|
1318
2014
|
}
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
function addOutsideRepoPath(paths, token, cwd, repoRoot) {
|
|
1322
|
-
const resolved = resolveMutationTarget(token, cwd);
|
|
1323
|
-
if (resolved && relativeWithinRepo(repoRoot, resolved) === null) {
|
|
1324
|
-
paths.add(resolved);
|
|
2015
|
+
if (path10.isAbsolute(token)) {
|
|
2016
|
+
return canonicalPath(token);
|
|
1325
2017
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
const tokens = tokenizeShell(command);
|
|
1329
|
-
const redirects = extractRedirectTargets(tokens);
|
|
1330
|
-
const paths = /* @__PURE__ */ new Set();
|
|
1331
|
-
for (const token of tokens.slice(1)) {
|
|
1332
|
-
addOutsideRepoPath(paths, token, cwd, repoRoot);
|
|
2018
|
+
if (token.startsWith("./") || token.startsWith("../")) {
|
|
2019
|
+
return canonicalPath(path10.resolve(cwd, token));
|
|
1333
2020
|
}
|
|
1334
|
-
|
|
1335
|
-
|
|
2021
|
+
if (!token.includes("/") && !token.includes("\\")) {
|
|
2022
|
+
return canonicalPath(path10.resolve(cwd, token));
|
|
1336
2023
|
}
|
|
1337
|
-
return
|
|
2024
|
+
return canonicalPath(path10.resolve(cwd, token));
|
|
1338
2025
|
}
|
|
1339
|
-
function
|
|
1340
|
-
const
|
|
1341
|
-
|
|
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;
|
|
2126
|
+
}
|
|
2127
|
+
function addOutsideRepoPath(paths, token, cwd, repoRoot, trustedWorkspaceRoots = []) {
|
|
2128
|
+
const resolved = resolveMutationTarget(token, cwd);
|
|
2129
|
+
if (resolved && resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolved) === null) {
|
|
2130
|
+
paths.add(resolved);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
function collectOutsideRepoPaths(command, cwd, repoRoot, trustedWorkspaceRoots = []) {
|
|
2134
|
+
const tokens = tokenizeShell(command);
|
|
2135
|
+
const redirects = extractRedirectTargets(tokens);
|
|
2136
|
+
const paths = /* @__PURE__ */ new Set();
|
|
2137
|
+
for (const token of tokens.slice(1)) {
|
|
2138
|
+
addOutsideRepoPath(paths, token, cwd, repoRoot, trustedWorkspaceRoots);
|
|
2139
|
+
}
|
|
2140
|
+
for (const redirect of redirects) {
|
|
2141
|
+
addOutsideRepoPath(paths, redirect, cwd, repoRoot, trustedWorkspaceRoots);
|
|
2142
|
+
}
|
|
2143
|
+
return [...paths];
|
|
2144
|
+
}
|
|
2145
|
+
function collectOutsideRepoPathsFromToolPayload(payload, cwd, repoRoot, trustedWorkspaceRoots = []) {
|
|
2146
|
+
const toolKind = String(payload.tool_name ?? "").trim().toLowerCase();
|
|
2147
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1342
2148
|
const filePath = extractToolFilePath(payload);
|
|
1343
2149
|
if (filePath) {
|
|
1344
|
-
const resolved =
|
|
1345
|
-
if (
|
|
2150
|
+
const resolved = path12.isAbsolute(filePath) ? filePath : path12.resolve(cwd, filePath);
|
|
2151
|
+
if (resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolved) === null) {
|
|
1346
2152
|
paths.add(resolved);
|
|
1347
2153
|
}
|
|
1348
2154
|
return [...paths];
|
|
@@ -1351,8 +2157,8 @@ function collectOutsideRepoPathsFromToolPayload(payload, cwd, repoRoot) {
|
|
|
1351
2157
|
const patch = extractToolPatch(payload);
|
|
1352
2158
|
if (patch) {
|
|
1353
2159
|
for (const target of applyPatchTargets(patch)) {
|
|
1354
|
-
const resolved =
|
|
1355
|
-
if (
|
|
2160
|
+
const resolved = path12.isAbsolute(target) ? target : path12.resolve(cwd, target);
|
|
2161
|
+
if (resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolved) === null) {
|
|
1356
2162
|
paths.add(resolved);
|
|
1357
2163
|
}
|
|
1358
2164
|
}
|
|
@@ -1375,162 +2181,123 @@ function shouldSkipBrokerApprovedRecord(brokerActive, approvalReason) {
|
|
|
1375
2181
|
return brokerActive && approvalReason !== void 0 && FS_SCOPE_REASONS.has(approvalReason);
|
|
1376
2182
|
}
|
|
1377
2183
|
|
|
1378
|
-
// src/core/
|
|
1379
|
-
import
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
unknownLocalEffect: "deny",
|
|
1388
|
-
unparseableShell: "deny",
|
|
1389
|
-
confidenceThresholds: { allow: 0.9, flag: 0.8 },
|
|
1390
|
-
modelAssist: { enabled: false }
|
|
1391
|
-
},
|
|
1392
|
-
sandbox: { ...DEFAULT_CONFIG_V3.sandbox }
|
|
1393
|
-
},
|
|
1394
|
-
standard: {
|
|
1395
|
-
mode: "enforce"
|
|
1396
|
-
},
|
|
1397
|
-
"audit-first": {
|
|
1398
|
-
mode: "audit",
|
|
1399
|
-
policy: {
|
|
1400
|
-
...DEFAULT_CONFIG_V3.policy,
|
|
1401
|
-
unknownLocalEffect: "deny",
|
|
1402
|
-
unparseableShell: "deny",
|
|
1403
|
-
confidenceThresholds: { allow: 0.88, flag: 0.72 },
|
|
1404
|
-
modelAssist: { enabled: false }
|
|
1405
|
-
},
|
|
1406
|
-
sandbox: { ...DEFAULT_CONFIG_V3.sandbox }
|
|
1407
|
-
},
|
|
1408
|
-
"l1-full-recommended": {
|
|
1409
|
-
mode: "enforce",
|
|
1410
|
-
policy: {
|
|
1411
|
-
...DEFAULT_CONFIG_V3.policy,
|
|
1412
|
-
confidenceThresholds: { ...DEFAULT_CONFIG_V3.policy.confidenceThresholds },
|
|
1413
|
-
modelAssist: { ...DEFAULT_CONFIG_V3.policy.modelAssist }
|
|
1414
|
-
},
|
|
1415
|
-
sandbox: {
|
|
1416
|
-
enabled: true,
|
|
1417
|
-
runtime: "container",
|
|
1418
|
-
denyNetworkByDefault: true
|
|
1419
|
-
},
|
|
1420
|
-
egress: {
|
|
1421
|
-
...DEFAULT_CONFIG_V3.egress,
|
|
1422
|
-
enabled: true,
|
|
1423
|
-
demoteL3External: true
|
|
1424
|
-
},
|
|
1425
|
-
approvalSigning: {
|
|
1426
|
-
required: true
|
|
1427
|
-
},
|
|
1428
|
-
controlPlane: {
|
|
1429
|
-
...DEFAULT_CONFIG_V3.controlPlane,
|
|
1430
|
-
isolation: {
|
|
1431
|
-
mode: "separate-user",
|
|
1432
|
-
verifyAgentWritable: true
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
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: [] };
|
|
1435
2193
|
}
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
...DEFAULT_CONFIG_V3.sandbox,
|
|
1450
|
-
...base.sandbox ?? {},
|
|
1451
|
-
...extra.sandbox
|
|
1452
|
-
},
|
|
1453
|
-
egress: {
|
|
1454
|
-
...DEFAULT_CONFIG_V3.egress,
|
|
1455
|
-
...base.egress ?? {},
|
|
1456
|
-
...extra.egress
|
|
1457
|
-
},
|
|
1458
|
-
approvalSigning: {
|
|
1459
|
-
...DEFAULT_CONFIG_V3.approvalSigning,
|
|
1460
|
-
...base.approvalSigning ?? {},
|
|
1461
|
-
...extra.approvalSigning
|
|
1462
|
-
},
|
|
1463
|
-
controlPlane: {
|
|
1464
|
-
...DEFAULT_CONFIG_V3.controlPlane,
|
|
1465
|
-
...base.controlPlane ?? {},
|
|
1466
|
-
...extra.controlPlane,
|
|
1467
|
-
isolation: {
|
|
1468
|
-
...DEFAULT_CONFIG_V3.controlPlane.isolation,
|
|
1469
|
-
...base.controlPlane?.isolation ?? {},
|
|
1470
|
-
...extra.controlPlane?.isolation ?? {}
|
|
1471
|
-
}
|
|
2194
|
+
const raw = JSON.parse(readFileSync2(filePath, "utf8"));
|
|
2195
|
+
return { version: 1, roots: sanitizeTrustedWorkspaceRootEntries(raw.roots) };
|
|
2196
|
+
}
|
|
2197
|
+
function normalizeTrustedWorkspaceRootPath(targetPath) {
|
|
2198
|
+
return canonicalPath(targetPath);
|
|
2199
|
+
}
|
|
2200
|
+
function sanitizeTrustedWorkspaceRootEntries(input) {
|
|
2201
|
+
if (!Array.isArray(input)) {
|
|
2202
|
+
return [];
|
|
2203
|
+
}
|
|
2204
|
+
return input.flatMap((entry) => {
|
|
2205
|
+
if (!entry || typeof entry !== "object") {
|
|
2206
|
+
return [];
|
|
1472
2207
|
}
|
|
1473
|
-
|
|
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
|
+
});
|
|
1474
2224
|
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
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;
|
|
2241
|
+
}
|
|
1481
2242
|
}
|
|
1482
|
-
function
|
|
1483
|
-
const
|
|
1484
|
-
|
|
1485
|
-
|
|
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;
|
|
1486
2248
|
}
|
|
1487
|
-
if (
|
|
1488
|
-
|
|
2249
|
+
if (home && normalizeTrustedWorkspaceRootPath(home) === root) {
|
|
2250
|
+
return true;
|
|
1489
2251
|
}
|
|
1490
|
-
return
|
|
1491
|
-
...config,
|
|
1492
|
-
controlPlane
|
|
1493
|
-
};
|
|
2252
|
+
return false;
|
|
1494
2253
|
}
|
|
1495
|
-
function
|
|
1496
|
-
|
|
1497
|
-
|
|
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;
|
|
1498
2260
|
}
|
|
1499
|
-
|
|
2261
|
+
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
2262
|
+
if (!home) {
|
|
2263
|
+
return false;
|
|
2264
|
+
}
|
|
2265
|
+
const homeRoot = normalizeTrustedWorkspaceRootPath(home);
|
|
2266
|
+
if (!pathWithinRoot(homeRoot, normalized)) {
|
|
2267
|
+
return false;
|
|
2268
|
+
}
|
|
2269
|
+
return HOME_HIGH_STAKES_SEGMENTS.some(
|
|
2270
|
+
(segment) => pathWithinRoot(path13.join(homeRoot, segment), normalized)
|
|
2271
|
+
);
|
|
1500
2272
|
}
|
|
1501
|
-
function
|
|
1502
|
-
const
|
|
1503
|
-
if (!
|
|
1504
|
-
return {
|
|
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" };
|
|
1505
2277
|
}
|
|
1506
|
-
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
if (JSON.stringify(protectedConfig) !== JSON.stringify(config)) {
|
|
1530
|
-
provenance.push({ path: "(protected-layer)", source: "protected" });
|
|
1531
|
-
config = protectedConfig;
|
|
1532
|
-
}
|
|
1533
|
-
return { config, provenance };
|
|
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 };
|
|
1534
2301
|
}
|
|
1535
2302
|
|
|
1536
2303
|
// src/core/gate-contract.ts
|
|
@@ -1575,30 +2342,6 @@ function unnormalizedGateVerdict(params) {
|
|
|
1575
2342
|
};
|
|
1576
2343
|
}
|
|
1577
2344
|
|
|
1578
|
-
// src/core/fingerprint.ts
|
|
1579
|
-
import { createHash } from "node:crypto";
|
|
1580
|
-
function canonicalStringify(value) {
|
|
1581
|
-
if (value === null || typeof value !== "object") {
|
|
1582
|
-
return JSON.stringify(value);
|
|
1583
|
-
}
|
|
1584
|
-
if (Array.isArray(value)) {
|
|
1585
|
-
return `[${value.map((item) => canonicalStringify(item)).join(",")}]`;
|
|
1586
|
-
}
|
|
1587
|
-
const entries = Object.entries(value).sort(
|
|
1588
|
-
([left], [right]) => left.localeCompare(right)
|
|
1589
|
-
);
|
|
1590
|
-
return `{${entries.map(([key, child]) => `${JSON.stringify(key)}:${canonicalStringify(child)}`).join(",")}}`;
|
|
1591
|
-
}
|
|
1592
|
-
function hashValue(value) {
|
|
1593
|
-
return createHash("sha256").update(value).digest("hex");
|
|
1594
|
-
}
|
|
1595
|
-
function subagentFingerprint(kind, scrubbed, repoRoot) {
|
|
1596
|
-
return hashValue(`subagent:${kind}:${canonicalStringify(scrubbed)}:${repoRoot}`);
|
|
1597
|
-
}
|
|
1598
|
-
function toolFingerprint(toolName, scrubbed, repoRoot) {
|
|
1599
|
-
return hashValue(`tool:${toolName}:${canonicalStringify(scrubbed)}:${repoRoot}`);
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
2345
|
// src/core/scrub.ts
|
|
1603
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;
|
|
1604
2347
|
var TIMESTAMP_PATTERN = /\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\b/g;
|
|
@@ -1684,6 +2427,51 @@ function scrubValue(value, options = DEFAULT_SCRUB_OPTIONS) {
|
|
|
1684
2427
|
return value;
|
|
1685
2428
|
}
|
|
1686
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
|
+
|
|
1687
2475
|
// src/core/classify-subagent.ts
|
|
1688
2476
|
var EXTERNAL_TERMS = ["deploy", "production", "publish", "release", "ship", "notify", "email"];
|
|
1689
2477
|
function extractSubagentText(payload, options) {
|
|
@@ -1707,32 +2495,7 @@ function extractSubagentText(payload, options) {
|
|
|
1707
2495
|
return canonicalStringify(scrubValue(payload, options.scrubOptions));
|
|
1708
2496
|
}
|
|
1709
2497
|
function fingerprintSource(payload, options) {
|
|
1710
|
-
|
|
1711
|
-
if (toolInput && typeof toolInput === "object") {
|
|
1712
|
-
const input = toolInput;
|
|
1713
|
-
return scrubValue(
|
|
1714
|
-
{
|
|
1715
|
-
description: input.description ?? "",
|
|
1716
|
-
prompt: input.prompt ?? ""
|
|
1717
|
-
},
|
|
1718
|
-
options.scrubOptions
|
|
1719
|
-
);
|
|
1720
|
-
}
|
|
1721
|
-
const task = payload.task;
|
|
1722
|
-
if (typeof task === "string") {
|
|
1723
|
-
return scrubValue({ task }, options.scrubOptions);
|
|
1724
|
-
}
|
|
1725
|
-
if (task && typeof task === "object") {
|
|
1726
|
-
const taskObj = task;
|
|
1727
|
-
return scrubValue(
|
|
1728
|
-
{
|
|
1729
|
-
description: taskObj.description ?? "",
|
|
1730
|
-
prompt: taskObj.prompt ?? ""
|
|
1731
|
-
},
|
|
1732
|
-
options.scrubOptions
|
|
1733
|
-
);
|
|
1734
|
-
}
|
|
1735
|
-
return scrubValue(payload, options.scrubOptions);
|
|
2498
|
+
return subagentFingerprintSource(payload, options.scrubOptions ?? {});
|
|
1736
2499
|
}
|
|
1737
2500
|
function classifySubagent(payload, repoRoot, options = {}) {
|
|
1738
2501
|
const kind = payload.tool_name === "Task" ? "Task" : String(payload.subagent_type ?? "generalPurpose");
|
|
@@ -1760,7 +2523,7 @@ function classifySubagent(payload, repoRoot, options = {}) {
|
|
|
1760
2523
|
}
|
|
1761
2524
|
|
|
1762
2525
|
// src/core/classify-tool.ts
|
|
1763
|
-
import
|
|
2526
|
+
import path21 from "node:path";
|
|
1764
2527
|
|
|
1765
2528
|
// src/core/glob.ts
|
|
1766
2529
|
function matchesSensitivePath(filePath, patterns) {
|
|
@@ -1806,6 +2569,10 @@ function matchesSensitivePath(filePath, patterns) {
|
|
|
1806
2569
|
}
|
|
1807
2570
|
|
|
1808
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
|
+
}
|
|
1809
2576
|
function judgeTraceAuditFields(trace) {
|
|
1810
2577
|
if (!trace) {
|
|
1811
2578
|
return {};
|
|
@@ -1816,120 +2583,40 @@ function judgeTraceAuditFields(trace) {
|
|
|
1816
2583
|
judgeModelResolved: trace.modelResolved,
|
|
1817
2584
|
judgeLatencyMs: trace.latencyMs,
|
|
1818
2585
|
...trace.outboundRedacted !== void 0 ? { judgeOutboundRedacted: trace.outboundRedacted } : {},
|
|
1819
|
-
...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 } : {}
|
|
1820
2598
|
};
|
|
1821
2599
|
}
|
|
1822
2600
|
|
|
1823
|
-
// src/
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
var claudeLayout = {
|
|
1830
|
-
name: "claude",
|
|
1831
|
-
configPath(repoRoot) {
|
|
1832
|
-
return path9.join(repoRoot, ".claude", "belay.config.json");
|
|
1833
|
-
},
|
|
1834
|
-
hooksSettingsPath(repoRoot) {
|
|
1835
|
-
return path9.join(repoRoot, ".claude", "settings.json");
|
|
1836
|
-
},
|
|
1837
|
-
hooksDir(repoRoot) {
|
|
1838
|
-
return path9.join(repoRoot, ".claude", "hooks");
|
|
1839
|
-
},
|
|
1840
|
-
runtimeDir(repoRoot) {
|
|
1841
|
-
return path9.join(repoRoot, ".claude", "belay", "runtime");
|
|
1842
|
-
},
|
|
1843
|
-
repoLocalStateDir(repoRoot) {
|
|
1844
|
-
return path9.join(repoRoot, ".claude", "belay");
|
|
1845
|
-
},
|
|
1846
|
-
defaultAuditLogPath(_repoRoot) {
|
|
1847
|
-
return path9.join(".claude", "belay", "audit.ndjson");
|
|
1848
|
-
},
|
|
1849
|
-
repoRootMarkers: [".git", ".claude"],
|
|
1850
|
-
runnerCommand: runnerCommand2,
|
|
1851
|
-
defaultConfig(repoRoot) {
|
|
1852
|
-
return {
|
|
1853
|
-
...DEFAULT_CONFIG_V4,
|
|
1854
|
-
adapter: "claude",
|
|
1855
|
-
audit: {
|
|
1856
|
-
...DEFAULT_CONFIG_V4.audit,
|
|
1857
|
-
logPath: claudeLayout.defaultAuditLogPath(repoRoot)
|
|
1858
|
-
}
|
|
1859
|
-
};
|
|
1860
|
-
}
|
|
1861
|
-
};
|
|
1862
|
-
|
|
1863
|
-
// src/adapters/layouts/cursor.ts
|
|
1864
|
-
import path10 from "node:path";
|
|
1865
|
-
function runnerCommand3(platform, repoRoot, hookName, ...args) {
|
|
1866
|
-
const hooksDir = path10.join(path10.resolve(repoRoot), ".cursor", "hooks");
|
|
1867
|
-
return buildRunnerInvocation(platform, hooksDir, repoRoot, hookName, ...args);
|
|
1868
|
-
}
|
|
1869
|
-
var cursorLayout = {
|
|
1870
|
-
name: "cursor",
|
|
1871
|
-
configPath(repoRoot) {
|
|
1872
|
-
return path10.join(repoRoot, ".cursor", "belay.config.json");
|
|
1873
|
-
},
|
|
1874
|
-
hooksSettingsPath(repoRoot) {
|
|
1875
|
-
return path10.join(repoRoot, ".cursor", "hooks.json");
|
|
1876
|
-
},
|
|
1877
|
-
hooksDir(repoRoot) {
|
|
1878
|
-
return path10.join(repoRoot, ".cursor", "hooks");
|
|
1879
|
-
},
|
|
1880
|
-
runtimeDir(repoRoot) {
|
|
1881
|
-
return path10.join(repoRoot, ".cursor", "belay", "runtime");
|
|
1882
|
-
},
|
|
1883
|
-
repoLocalStateDir(repoRoot) {
|
|
1884
|
-
return path10.join(repoRoot, ".cursor", "belay");
|
|
1885
|
-
},
|
|
1886
|
-
defaultAuditLogPath(_repoRoot) {
|
|
1887
|
-
return path10.join(".cursor", "belay", "audit.ndjson");
|
|
1888
|
-
},
|
|
1889
|
-
repoRootMarkers: [".git", ".cursor"],
|
|
1890
|
-
runnerCommand: runnerCommand3,
|
|
1891
|
-
defaultConfig(repoRoot) {
|
|
1892
|
-
return {
|
|
1893
|
-
...DEFAULT_CONFIG_V4,
|
|
1894
|
-
adapter: "cursor",
|
|
1895
|
-
audit: {
|
|
1896
|
-
...DEFAULT_CONFIG_V4.audit,
|
|
1897
|
-
logPath: cursorLayout.defaultAuditLogPath(repoRoot)
|
|
1898
|
-
}
|
|
1899
|
-
};
|
|
1900
|
-
}
|
|
1901
|
-
};
|
|
1902
|
-
|
|
1903
|
-
// src/adapters/layouts/index.ts
|
|
1904
|
-
var layouts = {
|
|
1905
|
-
cursor: cursorLayout,
|
|
1906
|
-
claude: claudeLayout,
|
|
1907
|
-
codex: codexLayout
|
|
1908
|
-
};
|
|
1909
|
-
function getAdapterLayout(name = "cursor") {
|
|
1910
|
-
return layouts[name];
|
|
1911
|
-
}
|
|
1912
|
-
|
|
1913
|
-
// src/config-io.ts
|
|
1914
|
-
function resolveAdapterName(config) {
|
|
1915
|
-
if (config.adapter === "claude") {
|
|
1916
|
-
return "claude";
|
|
2601
|
+
// src/core/verdict/judge-baseline.ts
|
|
2602
|
+
var samples = [];
|
|
2603
|
+
var MAX_SAMPLES = 2e3;
|
|
2604
|
+
function recordJudgeLatency(phase, latencyMs, at = Date.now()) {
|
|
2605
|
+
if (!Number.isFinite(latencyMs) || latencyMs < 0) {
|
|
2606
|
+
return;
|
|
1917
2607
|
}
|
|
1918
|
-
|
|
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;
|
|
2609
|
-
}
|
|
2610
|
-
function tier1RequiresAsk(verdict2) {
|
|
2611
|
-
return !verdict2.local_recoverable || verdict2.destroys_history_or_secrets;
|
|
3300
|
+
return judge;
|
|
3301
|
+
}
|
|
3302
|
+
function tier1RequiresAsk(verdict2) {
|
|
3303
|
+
return !verdict2.local_recoverable || verdict2.destroys_history_or_secrets;
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
// src/core/verdict/judge-cli.ts
|
|
3307
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
3308
|
+
|
|
3309
|
+
// src/core/verdict/judge-session-guard.ts
|
|
3310
|
+
function buildJudgeSessionKey(parts) {
|
|
3311
|
+
return [parts.providerId, parts.model, parts.repoRoot, parts.judgeMode, parts.cliVersion].join(
|
|
3312
|
+
":"
|
|
3313
|
+
);
|
|
3314
|
+
}
|
|
3315
|
+
function evaluateSessionReuse(existing, requested, budget, config, now = Date.now(), incomingPromptBytes = 0) {
|
|
3316
|
+
const sessionKey = buildJudgeSessionKey(requested);
|
|
3317
|
+
if (!config.enabled) {
|
|
3318
|
+
return { canReuse: false, resetReason: "session_disabled", sessionKey };
|
|
3319
|
+
}
|
|
3320
|
+
if (!config.providerAllowlist.includes(requested.providerId)) {
|
|
3321
|
+
return { canReuse: false, resetReason: "provider_not_allowlisted", sessionKey };
|
|
3322
|
+
}
|
|
3323
|
+
if (!existing || !budget) {
|
|
3324
|
+
return { canReuse: false, resetReason: "initial", sessionKey };
|
|
3325
|
+
}
|
|
3326
|
+
const existingKey = buildJudgeSessionKey(existing);
|
|
3327
|
+
if (existingKey !== sessionKey) {
|
|
3328
|
+
if (existing.providerId !== requested.providerId) {
|
|
3329
|
+
return { canReuse: false, resetReason: "provider_mismatch", sessionKey };
|
|
3330
|
+
}
|
|
3331
|
+
if (existing.model !== requested.model) {
|
|
3332
|
+
return { canReuse: false, resetReason: "model_mismatch", sessionKey };
|
|
3333
|
+
}
|
|
3334
|
+
if (existing.repoRoot !== requested.repoRoot) {
|
|
3335
|
+
return { canReuse: false, resetReason: "repo_mismatch", sessionKey };
|
|
3336
|
+
}
|
|
3337
|
+
if (existing.judgeMode !== requested.judgeMode) {
|
|
3338
|
+
return { canReuse: false, resetReason: "mode_mismatch", sessionKey };
|
|
3339
|
+
}
|
|
3340
|
+
if (existing.cliVersion !== requested.cliVersion) {
|
|
3341
|
+
return { canReuse: false, resetReason: "cli_version_mismatch", sessionKey };
|
|
3342
|
+
}
|
|
3343
|
+
return { canReuse: false, resetReason: "provider_mismatch", sessionKey };
|
|
3344
|
+
}
|
|
3345
|
+
if (budget.turnCount >= config.maxTurns) {
|
|
3346
|
+
return { canReuse: false, resetReason: "max_turns_exceeded", sessionKey };
|
|
3347
|
+
}
|
|
3348
|
+
if (now - budget.createdAtMs >= config.maxAgeMs) {
|
|
3349
|
+
return { canReuse: false, resetReason: "max_age_exceeded", sessionKey };
|
|
3350
|
+
}
|
|
3351
|
+
if (now - budget.lastUsedAtMs >= config.maxIdleMs) {
|
|
3352
|
+
return { canReuse: false, resetReason: "max_idle_exceeded", sessionKey };
|
|
3353
|
+
}
|
|
3354
|
+
if (budget.promptBytes + incomingPromptBytes > config.maxPromptBytes) {
|
|
3355
|
+
return { canReuse: false, resetReason: "max_prompt_bytes_exceeded", sessionKey };
|
|
3356
|
+
}
|
|
3357
|
+
return { canReuse: true, sessionKey };
|
|
3358
|
+
}
|
|
3359
|
+
function exceedsPromptBudget(promptBytes, config, existingBudget = null) {
|
|
3360
|
+
const total = (existingBudget?.promptBytes ?? 0) + promptBytes;
|
|
3361
|
+
return total > config.maxPromptBytes;
|
|
3362
|
+
}
|
|
3363
|
+
function guardFailClosedFallbackReason(resetReason) {
|
|
3364
|
+
switch (resetReason) {
|
|
3365
|
+
case "parse_failure":
|
|
3366
|
+
return "parse_error";
|
|
3367
|
+
case "non_json_response":
|
|
3368
|
+
return "non_json_response";
|
|
3369
|
+
case "cli_error":
|
|
3370
|
+
return "cli_error";
|
|
3371
|
+
case "timeout":
|
|
3372
|
+
return "timeout";
|
|
3373
|
+
case "cli_version_mismatch":
|
|
3374
|
+
return "version_mismatch";
|
|
3375
|
+
case "kill_switch":
|
|
3376
|
+
return "kill_switch";
|
|
3377
|
+
case "session_disabled":
|
|
3378
|
+
return "session_disabled";
|
|
3379
|
+
case "provider_not_allowlisted":
|
|
3380
|
+
return "provider_not_allowlisted";
|
|
3381
|
+
case "max_prompt_bytes_exceeded":
|
|
3382
|
+
return "guard_reset";
|
|
3383
|
+
default:
|
|
3384
|
+
return "guard_reset";
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
function transportFallbackToFailClosedReason(providerId, fallback) {
|
|
3388
|
+
if (!fallback) {
|
|
3389
|
+
return `${providerId}_cli_parse_error`;
|
|
3390
|
+
}
|
|
3391
|
+
if (fallback.endsWith("_cli_unavailable") || fallback.endsWith("_cli_parse_error")) {
|
|
3392
|
+
return fallback;
|
|
3393
|
+
}
|
|
3394
|
+
switch (fallback) {
|
|
3395
|
+
case "non_json_response":
|
|
3396
|
+
case "parse_error":
|
|
3397
|
+
case "parse_timeout":
|
|
3398
|
+
return `${providerId}_cli_parse_error`;
|
|
3399
|
+
default:
|
|
3400
|
+
return `${providerId}_cli_unavailable`;
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
// src/core/verdict/judge-broker-service.ts
|
|
3405
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
3406
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
3407
|
+
import { unlink as unlink2 } from "node:fs/promises";
|
|
3408
|
+
import net from "node:net";
|
|
3409
|
+
import path17 from "node:path";
|
|
3410
|
+
import { fileURLToPath } from "node:url";
|
|
3411
|
+
|
|
3412
|
+
// src/core/verdict/judge-session-mutex.ts
|
|
3413
|
+
var AsyncMutex = class {
|
|
3414
|
+
chain = Promise.resolve();
|
|
3415
|
+
run(fn) {
|
|
3416
|
+
const next = this.chain.then(fn);
|
|
3417
|
+
this.chain = next.then(
|
|
3418
|
+
() => void 0,
|
|
3419
|
+
() => void 0
|
|
3420
|
+
);
|
|
3421
|
+
return next;
|
|
3422
|
+
}
|
|
3423
|
+
};
|
|
3424
|
+
var MutexRegistry = class {
|
|
3425
|
+
mutexes = /* @__PURE__ */ new Map();
|
|
3426
|
+
forKey(key) {
|
|
3427
|
+
const existing = this.mutexes.get(key);
|
|
3428
|
+
if (existing) {
|
|
3429
|
+
return existing;
|
|
3430
|
+
}
|
|
3431
|
+
const created = new AsyncMutex();
|
|
3432
|
+
this.mutexes.set(key, created);
|
|
3433
|
+
return created;
|
|
3434
|
+
}
|
|
3435
|
+
clear() {
|
|
3436
|
+
this.mutexes.clear();
|
|
3437
|
+
}
|
|
3438
|
+
};
|
|
3439
|
+
|
|
3440
|
+
// src/core/verdict/judge-session-broker.ts
|
|
3441
|
+
var JudgeSessionBroker = class {
|
|
3442
|
+
sessions = /* @__PURE__ */ new Map();
|
|
3443
|
+
mutexes = new MutexRegistry();
|
|
3444
|
+
options;
|
|
3445
|
+
constructor(options) {
|
|
3446
|
+
this.options = options;
|
|
3447
|
+
}
|
|
3448
|
+
listSessions() {
|
|
3449
|
+
return [...this.sessions.values()];
|
|
3450
|
+
}
|
|
3451
|
+
invalidateSession(sessionKey, reason) {
|
|
3452
|
+
const session = this.sessions.get(sessionKey);
|
|
3453
|
+
if (session) {
|
|
3454
|
+
session.lastResetReason = reason;
|
|
3455
|
+
session.state = "idle";
|
|
3456
|
+
}
|
|
3457
|
+
this.sessions.delete(sessionKey);
|
|
3458
|
+
}
|
|
3459
|
+
stopAll(reason = "manual_stop") {
|
|
3460
|
+
const count = this.sessions.size;
|
|
3461
|
+
for (const session of this.sessions.values()) {
|
|
3462
|
+
session.lastResetReason = reason;
|
|
3463
|
+
session.state = "idle";
|
|
3464
|
+
}
|
|
3465
|
+
this.sessions.clear();
|
|
3466
|
+
this.mutexes.clear();
|
|
3467
|
+
return count;
|
|
3468
|
+
}
|
|
3469
|
+
async evaluate(request, timeoutMs) {
|
|
3470
|
+
const sessionKey = buildJudgeSessionKey(request.keyParts);
|
|
3471
|
+
return this.mutexes.forKey(sessionKey).run(() => this.evaluateExclusive(request, timeoutMs));
|
|
3472
|
+
}
|
|
3473
|
+
async evaluateExclusive(request, timeoutMs) {
|
|
3474
|
+
const sessionKey = buildJudgeSessionKey(request.keyParts);
|
|
3475
|
+
const providerId = request.keyParts.providerId;
|
|
3476
|
+
const existing = this.sessions.get(sessionKey);
|
|
3477
|
+
const reuseDecision = evaluateSessionReuse(
|
|
3478
|
+
existing?.keyParts ?? null,
|
|
3479
|
+
request.keyParts,
|
|
3480
|
+
existing?.budget ?? null,
|
|
3481
|
+
this.options.config,
|
|
3482
|
+
Date.now(),
|
|
3483
|
+
request.promptBytes
|
|
3484
|
+
);
|
|
3485
|
+
const resetReason = reuseDecision.canReuse ? void 0 : reuseDecision.resetReason;
|
|
3486
|
+
let session = existing;
|
|
3487
|
+
if (!reuseDecision.canReuse || !session) {
|
|
3488
|
+
session = {
|
|
3489
|
+
keyParts: request.keyParts,
|
|
3490
|
+
budget: {
|
|
3491
|
+
turnCount: 0,
|
|
3492
|
+
createdAtMs: Date.now(),
|
|
3493
|
+
lastUsedAtMs: Date.now(),
|
|
3494
|
+
promptBytes: 0
|
|
3495
|
+
},
|
|
3496
|
+
state: "busy",
|
|
3497
|
+
lastResetReason: resetReason ?? "initial"
|
|
3498
|
+
};
|
|
3499
|
+
this.enforceProviderSessionCap(providerId, sessionKey);
|
|
3500
|
+
this.sessions.set(sessionKey, session);
|
|
3501
|
+
} else {
|
|
3502
|
+
session.state = "busy";
|
|
3503
|
+
session.budget.lastUsedAtMs = Date.now();
|
|
3504
|
+
}
|
|
3505
|
+
const invocation = reuseDecision.canReuse && session.providerResumeId ? this.buildResumeInvocation(request.invocation, providerId, session.providerResumeId) : request.invocation;
|
|
3506
|
+
const resumed = reuseDecision.canReuse && Boolean(session.providerResumeId);
|
|
3507
|
+
try {
|
|
3508
|
+
const raw = await this.options.runCommand(invocation, timeoutMs);
|
|
3509
|
+
session.budget.turnCount += 1;
|
|
3510
|
+
session.budget.promptBytes += request.promptBytes;
|
|
3511
|
+
session.budget.lastUsedAtMs = Date.now();
|
|
3512
|
+
session.state = "idle";
|
|
3513
|
+
const extracted = this.options.extractResumeId?.(providerId, raw) ?? extractProviderResumeId(providerId, raw) ?? void 0;
|
|
3514
|
+
if (extracted) {
|
|
3515
|
+
session.providerResumeId = extracted;
|
|
3516
|
+
}
|
|
3517
|
+
return {
|
|
3518
|
+
raw,
|
|
3519
|
+
reused: resumed,
|
|
3520
|
+
resetReason: session.lastResetReason,
|
|
3521
|
+
providerResumeId: session.providerResumeId
|
|
3522
|
+
};
|
|
3523
|
+
} catch (error) {
|
|
3524
|
+
session.state = "idle";
|
|
3525
|
+
session.lastResetReason = "cli_error";
|
|
3526
|
+
this.sessions.delete(sessionKey);
|
|
3527
|
+
throw error;
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
enforceProviderSessionCap(providerId, keepKey) {
|
|
3531
|
+
const cap = this.options.config.maxSessionsPerProvider;
|
|
3532
|
+
const sameProvider = [...this.sessions.entries()].filter(
|
|
3533
|
+
([, record]) => record.keyParts.providerId === providerId
|
|
3534
|
+
);
|
|
3535
|
+
if (sameProvider.length < cap) {
|
|
3536
|
+
return;
|
|
3537
|
+
}
|
|
3538
|
+
for (const [key] of sameProvider) {
|
|
3539
|
+
if (key === keepKey) {
|
|
3540
|
+
continue;
|
|
3541
|
+
}
|
|
3542
|
+
this.sessions.delete(key);
|
|
3543
|
+
if (this.sessions.size < cap) {
|
|
3544
|
+
break;
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
buildResumeInvocation(base, providerId, resumeId) {
|
|
3549
|
+
if (providerId === "cursor") {
|
|
3550
|
+
const args = [...base.args];
|
|
3551
|
+
const modelIndex = args.indexOf("--model");
|
|
3552
|
+
const model = modelIndex >= 0 ? args[modelIndex + 1] : void 0;
|
|
3553
|
+
const prompt = args[args.length - 1];
|
|
3554
|
+
return {
|
|
3555
|
+
binary: base.binary,
|
|
3556
|
+
args: [
|
|
3557
|
+
"--print",
|
|
3558
|
+
"--output-format",
|
|
3559
|
+
"json",
|
|
3560
|
+
"--mode",
|
|
3561
|
+
"ask",
|
|
3562
|
+
"--sandbox",
|
|
3563
|
+
"enabled",
|
|
3564
|
+
"--trust",
|
|
3565
|
+
"--resume",
|
|
3566
|
+
resumeId,
|
|
3567
|
+
...model ? ["--model", model] : [],
|
|
3568
|
+
prompt
|
|
3569
|
+
]
|
|
3570
|
+
};
|
|
3571
|
+
}
|
|
3572
|
+
if (providerId === "claude") {
|
|
3573
|
+
return {
|
|
3574
|
+
...base,
|
|
3575
|
+
args: [...base.args, "--continue", resumeId]
|
|
3576
|
+
};
|
|
3577
|
+
}
|
|
3578
|
+
return base;
|
|
3579
|
+
}
|
|
3580
|
+
};
|
|
3581
|
+
function extractProviderResumeId(providerId, raw) {
|
|
3582
|
+
try {
|
|
3583
|
+
const parsed = JSON.parse(raw);
|
|
3584
|
+
const candidates = [
|
|
3585
|
+
parsed.chat_id,
|
|
3586
|
+
parsed.chatId,
|
|
3587
|
+
parsed.session_id,
|
|
3588
|
+
parsed.sessionId,
|
|
3589
|
+
parsed.result?.chat_id,
|
|
3590
|
+
parsed.result?.session_id
|
|
3591
|
+
];
|
|
3592
|
+
for (const candidate of candidates) {
|
|
3593
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
3594
|
+
return candidate.trim();
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
} catch {
|
|
3598
|
+
}
|
|
3599
|
+
if (providerId === "cursor") {
|
|
3600
|
+
const match = raw.match(/"chat_id"\s*:\s*"([^"]+)"/);
|
|
3601
|
+
if (match?.[1]) {
|
|
3602
|
+
return match[1];
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
return null;
|
|
3606
|
+
}
|
|
3607
|
+
var repoBrokers = /* @__PURE__ */ new Map();
|
|
3608
|
+
function getRepoJudgeSessionBroker(repoRoot, options) {
|
|
3609
|
+
const existing = repoBrokers.get(repoRoot);
|
|
3610
|
+
if (existing) {
|
|
3611
|
+
return existing;
|
|
3612
|
+
}
|
|
3613
|
+
const broker = new JudgeSessionBroker(options);
|
|
3614
|
+
repoBrokers.set(repoRoot, broker);
|
|
3615
|
+
return broker;
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3618
|
+
// src/core/verdict/judge-broker-service.ts
|
|
3619
|
+
var JUDGE_BROKER_SOCKET = "judge-broker.sock";
|
|
3620
|
+
var JUDGE_BROKER_STATUS = "judge-broker.json";
|
|
3621
|
+
var JUDGE_BROKER_PID = "judge-broker.pid";
|
|
3622
|
+
var JUDGE_BROKER_SESSION = "judge-broker-session.json";
|
|
3623
|
+
function judgeBrokerPaths(stateDir) {
|
|
3624
|
+
return {
|
|
3625
|
+
stateDir,
|
|
3626
|
+
socketPath: path17.join(stateDir, JUDGE_BROKER_SOCKET),
|
|
3627
|
+
statusPath: path17.join(stateDir, JUDGE_BROKER_STATUS),
|
|
3628
|
+
pidPath: path17.join(stateDir, JUDGE_BROKER_PID),
|
|
3629
|
+
sessionConfigPath: path17.join(stateDir, JUDGE_BROKER_SESSION)
|
|
3630
|
+
};
|
|
3631
|
+
}
|
|
3632
|
+
function daemonScriptPath() {
|
|
3633
|
+
return fileURLToPath(new URL("../../judge-broker-daemon.js", import.meta.url));
|
|
3634
|
+
}
|
|
3635
|
+
function isProcessAlive(pid) {
|
|
3636
|
+
try {
|
|
3637
|
+
process.kill(pid, 0);
|
|
3638
|
+
return true;
|
|
3639
|
+
} catch {
|
|
3640
|
+
return false;
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
function useInProcessBroker(env = process.env) {
|
|
3644
|
+
return Boolean(env.VITEST || env.VITEST_WORKER_ID || env.BELAY_JUDGE_BROKER_IN_PROCESS === "1");
|
|
3645
|
+
}
|
|
3646
|
+
async function readBrokerStatus(statusPath) {
|
|
3647
|
+
if (!existsSync5(statusPath)) {
|
|
3648
|
+
return null;
|
|
3649
|
+
}
|
|
3650
|
+
try {
|
|
3651
|
+
const { readFile: readFile6 } = await import("node:fs/promises");
|
|
3652
|
+
const raw = JSON.parse(await readFile6(statusPath, "utf8"));
|
|
3653
|
+
if (typeof raw.pid !== "number" || typeof raw.socketPath !== "string") {
|
|
3654
|
+
return null;
|
|
3655
|
+
}
|
|
3656
|
+
return raw;
|
|
3657
|
+
} catch {
|
|
3658
|
+
return null;
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
async function readBrokerSessionConfig(sessionConfigPath) {
|
|
3662
|
+
if (!existsSync5(sessionConfigPath)) {
|
|
3663
|
+
return null;
|
|
3664
|
+
}
|
|
3665
|
+
try {
|
|
3666
|
+
const { readFile: readFile6 } = await import("node:fs/promises");
|
|
3667
|
+
const raw = JSON.parse(
|
|
3668
|
+
await readFile6(sessionConfigPath, "utf8")
|
|
3669
|
+
);
|
|
3670
|
+
return normalizeJudgeSessionConfig({ ...raw, enabled: true });
|
|
3671
|
+
} catch {
|
|
3672
|
+
return null;
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
function brokerSessionConfigPayload(sessionConfig) {
|
|
3676
|
+
return JSON.stringify(normalizeJudgeSessionConfig({ ...sessionConfig, enabled: true }));
|
|
3677
|
+
}
|
|
3678
|
+
async function writeBrokerSessionConfig(paths, sessionConfig) {
|
|
3679
|
+
const { writeFile: writeFile6 } = await import("node:fs/promises");
|
|
3680
|
+
await writeFile6(paths.sessionConfigPath, brokerSessionConfigPayload(sessionConfig), {
|
|
3681
|
+
encoding: "utf8",
|
|
3682
|
+
mode: 384
|
|
3683
|
+
});
|
|
3684
|
+
}
|
|
3685
|
+
async function connectBrokerSocket(socketPath, timeoutMs) {
|
|
3686
|
+
return new Promise((resolve, reject) => {
|
|
3687
|
+
const socket = net.createConnection(socketPath);
|
|
3688
|
+
const timer = setTimeout(() => {
|
|
3689
|
+
socket.destroy();
|
|
3690
|
+
reject(new Error("judge broker connect timed out"));
|
|
3691
|
+
}, timeoutMs);
|
|
3692
|
+
socket.once("connect", () => {
|
|
3693
|
+
clearTimeout(timer);
|
|
3694
|
+
resolve(socket);
|
|
3695
|
+
});
|
|
3696
|
+
socket.once("error", (error) => {
|
|
3697
|
+
clearTimeout(timer);
|
|
3698
|
+
reject(error);
|
|
3699
|
+
});
|
|
3700
|
+
});
|
|
3701
|
+
}
|
|
3702
|
+
async function rpcEvaluate(socketPath, request, timeoutMs, connectTimeoutMs) {
|
|
3703
|
+
const socket = await connectBrokerSocket(socketPath, connectTimeoutMs);
|
|
3704
|
+
const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3705
|
+
const payload = JSON.stringify({
|
|
3706
|
+
type: "evaluate",
|
|
3707
|
+
id,
|
|
3708
|
+
request,
|
|
3709
|
+
timeoutMs
|
|
3710
|
+
});
|
|
3711
|
+
return new Promise((resolve, reject) => {
|
|
3712
|
+
let buffer = "";
|
|
3713
|
+
const timer = setTimeout(() => {
|
|
3714
|
+
socket.destroy();
|
|
3715
|
+
reject(new Error("judge broker evaluate timed out"));
|
|
3716
|
+
}, timeoutMs + connectTimeoutMs);
|
|
3717
|
+
const onData = (chunk) => {
|
|
3718
|
+
buffer += String(chunk);
|
|
3719
|
+
const lines = buffer.split("\n");
|
|
3720
|
+
buffer = lines.pop() ?? "";
|
|
3721
|
+
for (const line of lines) {
|
|
3722
|
+
if (!line.trim()) {
|
|
3723
|
+
continue;
|
|
3724
|
+
}
|
|
3725
|
+
try {
|
|
3726
|
+
const message = JSON.parse(line);
|
|
3727
|
+
if (message.id !== id) {
|
|
3728
|
+
continue;
|
|
3729
|
+
}
|
|
3730
|
+
clearTimeout(timer);
|
|
3731
|
+
socket.destroy();
|
|
3732
|
+
if (message.type === "error") {
|
|
3733
|
+
reject(new Error(message.error ?? "judge broker error"));
|
|
3734
|
+
return;
|
|
3735
|
+
}
|
|
3736
|
+
if (message.result) {
|
|
3737
|
+
resolve(message.result);
|
|
3738
|
+
}
|
|
3739
|
+
} catch {
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3742
|
+
};
|
|
3743
|
+
socket.on("data", onData);
|
|
3744
|
+
socket.on("error", (error) => {
|
|
3745
|
+
clearTimeout(timer);
|
|
3746
|
+
reject(error);
|
|
3747
|
+
});
|
|
3748
|
+
socket.write(`${payload}
|
|
3749
|
+
`);
|
|
3750
|
+
});
|
|
3751
|
+
}
|
|
3752
|
+
async function ensureJudgeBrokerDaemon(stateDir, repoRoot, sessionConfig) {
|
|
3753
|
+
const paths = judgeBrokerPaths(stateDir);
|
|
3754
|
+
const brokerSessionConfig = normalizeJudgeSessionConfig({ ...sessionConfig, enabled: true });
|
|
3755
|
+
const nextConfigPayload = brokerSessionConfigPayload(brokerSessionConfig);
|
|
3756
|
+
const existingConfig = await readBrokerSessionConfig(paths.sessionConfigPath);
|
|
3757
|
+
const configChanged = existingConfig !== null && brokerSessionConfigPayload(existingConfig) !== nextConfigPayload;
|
|
3758
|
+
const status = await readBrokerStatus(paths.statusPath);
|
|
3759
|
+
if (status && isProcessAlive(status.pid) && existsSync5(paths.socketPath) && !configChanged) {
|
|
3760
|
+
await writeBrokerSessionConfig(paths, brokerSessionConfig);
|
|
3761
|
+
return paths;
|
|
3762
|
+
}
|
|
3763
|
+
if (status?.pid && isProcessAlive(status.pid)) {
|
|
3764
|
+
await stopJudgeBrokerDaemon(stateDir);
|
|
3765
|
+
} else {
|
|
3766
|
+
await cleanupJudgeBrokerArtifacts(paths);
|
|
3767
|
+
}
|
|
3768
|
+
await writeBrokerSessionConfig(paths, brokerSessionConfig);
|
|
3769
|
+
const child = spawn2(process.execPath, [daemonScriptPath()], {
|
|
3770
|
+
detached: true,
|
|
3771
|
+
stdio: "ignore",
|
|
3772
|
+
env: {
|
|
3773
|
+
...process.env,
|
|
3774
|
+
BELAY_JUDGE_BROKER_REPO_ROOT: repoRoot,
|
|
3775
|
+
BELAY_JUDGE_BROKER_STATE_DIR: stateDir
|
|
3776
|
+
}
|
|
3777
|
+
});
|
|
3778
|
+
child.unref();
|
|
3779
|
+
const deadline = Date.now() + sessionConfig.connectTimeoutMs;
|
|
3780
|
+
while (Date.now() < deadline) {
|
|
3781
|
+
if (existsSync5(paths.socketPath)) {
|
|
3782
|
+
const live = await readBrokerStatus(paths.statusPath);
|
|
3783
|
+
if (live && isProcessAlive(live.pid)) {
|
|
3784
|
+
return paths;
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
3788
|
+
}
|
|
3789
|
+
throw new Error("judge broker daemon failed to start");
|
|
3790
|
+
}
|
|
3791
|
+
async function cleanupJudgeBrokerArtifacts(paths) {
|
|
3792
|
+
for (const artifact of [
|
|
3793
|
+
paths.socketPath,
|
|
3794
|
+
paths.statusPath,
|
|
3795
|
+
paths.pidPath,
|
|
3796
|
+
paths.sessionConfigPath
|
|
3797
|
+
]) {
|
|
3798
|
+
if (existsSync5(artifact)) {
|
|
3799
|
+
await unlink2(artifact).catch(() => void 0);
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
async function stopJudgeBrokerDaemon(stateDir) {
|
|
3804
|
+
const paths = judgeBrokerPaths(stateDir);
|
|
3805
|
+
const status = await readBrokerStatus(paths.statusPath);
|
|
3806
|
+
if (status?.pid && isProcessAlive(status.pid)) {
|
|
3807
|
+
try {
|
|
3808
|
+
process.kill(status.pid, "SIGTERM");
|
|
3809
|
+
} catch {
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
await cleanupJudgeBrokerArtifacts(paths);
|
|
3813
|
+
return status?.pid ? 1 : 0;
|
|
3814
|
+
}
|
|
3815
|
+
async function evaluateViaJudgeBroker(options) {
|
|
3816
|
+
const runCommand = options.runCommand ?? ((invocation, timeout) => runCliJsonWithTimeouts(invocation, { evalTimeoutMs: timeout }));
|
|
3817
|
+
if (useInProcessBroker()) {
|
|
3818
|
+
const broker = getRepoJudgeSessionBroker(options.repoRoot, {
|
|
3819
|
+
config: options.sessionConfig,
|
|
3820
|
+
runCommand
|
|
3821
|
+
});
|
|
3822
|
+
return broker.evaluate(options.request, options.timeoutMs);
|
|
3823
|
+
}
|
|
3824
|
+
const paths = await ensureJudgeBrokerDaemon(
|
|
3825
|
+
options.stateDir,
|
|
3826
|
+
options.repoRoot,
|
|
3827
|
+
options.sessionConfig
|
|
3828
|
+
);
|
|
3829
|
+
return rpcEvaluate(
|
|
3830
|
+
paths.socketPath,
|
|
3831
|
+
options.request,
|
|
3832
|
+
options.timeoutMs,
|
|
3833
|
+
options.sessionConfig.connectTimeoutMs
|
|
3834
|
+
);
|
|
3835
|
+
}
|
|
3836
|
+
async function rpcInvalidate(socketPath, sessionKey, reason, connectTimeoutMs) {
|
|
3837
|
+
const socket = await connectBrokerSocket(socketPath, connectTimeoutMs);
|
|
3838
|
+
const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3839
|
+
const payload = JSON.stringify({ type: "invalidate", id, sessionKey, reason });
|
|
3840
|
+
await new Promise((resolve, reject) => {
|
|
3841
|
+
let buffer = "";
|
|
3842
|
+
const timer = setTimeout(() => {
|
|
3843
|
+
socket.destroy();
|
|
3844
|
+
reject(new Error("judge broker invalidate timed out"));
|
|
3845
|
+
}, connectTimeoutMs);
|
|
3846
|
+
const onData = (chunk) => {
|
|
3847
|
+
buffer += String(chunk);
|
|
3848
|
+
const lines = buffer.split("\n");
|
|
3849
|
+
buffer = lines.pop() ?? "";
|
|
3850
|
+
for (const line of lines) {
|
|
3851
|
+
if (!line.trim()) {
|
|
3852
|
+
continue;
|
|
3853
|
+
}
|
|
3854
|
+
try {
|
|
3855
|
+
const message = JSON.parse(line);
|
|
3856
|
+
if (message.id !== id) {
|
|
3857
|
+
continue;
|
|
3858
|
+
}
|
|
3859
|
+
clearTimeout(timer);
|
|
3860
|
+
socket.destroy();
|
|
3861
|
+
resolve();
|
|
3862
|
+
} catch {
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
};
|
|
3866
|
+
socket.on("data", onData);
|
|
3867
|
+
socket.on("error", (error) => {
|
|
3868
|
+
clearTimeout(timer);
|
|
3869
|
+
reject(error);
|
|
3870
|
+
});
|
|
3871
|
+
socket.write(`${payload}
|
|
3872
|
+
`);
|
|
3873
|
+
});
|
|
3874
|
+
}
|
|
3875
|
+
async function invalidateJudgeSession(options) {
|
|
3876
|
+
if (useInProcessBroker()) {
|
|
3877
|
+
const broker = getRepoJudgeSessionBroker(options.repoRoot, {
|
|
3878
|
+
config: { ...options.sessionConfig, enabled: true },
|
|
3879
|
+
runCommand: (invocation, timeout) => runCliJsonWithTimeouts(invocation, { evalTimeoutMs: timeout })
|
|
3880
|
+
});
|
|
3881
|
+
broker.invalidateSession(options.sessionKey, options.reason);
|
|
3882
|
+
return;
|
|
3883
|
+
}
|
|
3884
|
+
const paths = await ensureJudgeBrokerDaemon(
|
|
3885
|
+
options.stateDir,
|
|
3886
|
+
options.repoRoot,
|
|
3887
|
+
options.sessionConfig
|
|
3888
|
+
);
|
|
3889
|
+
await rpcInvalidate(
|
|
3890
|
+
paths.socketPath,
|
|
3891
|
+
options.sessionKey,
|
|
3892
|
+
options.reason,
|
|
3893
|
+
options.sessionConfig.connectTimeoutMs
|
|
3894
|
+
);
|
|
3895
|
+
}
|
|
3896
|
+
|
|
3897
|
+
// src/core/verdict/judge-cli-fingerprint.ts
|
|
3898
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
3899
|
+
var CLI_BINARIES = {
|
|
3900
|
+
codex: "codex",
|
|
3901
|
+
cursor: "cursor-agent",
|
|
3902
|
+
claude: "claude"
|
|
3903
|
+
};
|
|
3904
|
+
var fingerprintResolver = null;
|
|
3905
|
+
var fingerprintCache = /* @__PURE__ */ new Map();
|
|
3906
|
+
async function runVersionProbe(binary, timeoutMs) {
|
|
3907
|
+
return new Promise((resolve, reject) => {
|
|
3908
|
+
const child = spawn3(binary, ["--version"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
3909
|
+
let stdout = "";
|
|
3910
|
+
let stderr = "";
|
|
3911
|
+
const timer = setTimeout(() => {
|
|
3912
|
+
child.kill("SIGTERM");
|
|
3913
|
+
reject(new Error(`${binary} --version timed out`));
|
|
3914
|
+
}, timeoutMs);
|
|
3915
|
+
child.stdout.on("data", (chunk) => {
|
|
3916
|
+
stdout += String(chunk);
|
|
3917
|
+
});
|
|
3918
|
+
child.stderr.on("data", (chunk) => {
|
|
3919
|
+
stderr += String(chunk);
|
|
3920
|
+
});
|
|
3921
|
+
child.on("error", (error) => {
|
|
3922
|
+
clearTimeout(timer);
|
|
3923
|
+
reject(error);
|
|
3924
|
+
});
|
|
3925
|
+
child.on("close", (code) => {
|
|
3926
|
+
clearTimeout(timer);
|
|
3927
|
+
const output = (stdout || stderr).trim();
|
|
3928
|
+
if (code === 0 && output) {
|
|
3929
|
+
resolve(output.split(/\r?\n/)[0]?.trim() || output);
|
|
3930
|
+
return;
|
|
3931
|
+
}
|
|
3932
|
+
reject(
|
|
3933
|
+
new Error(stderr.trim() || `${binary} --version exited with code ${code ?? "unknown"}`)
|
|
3934
|
+
);
|
|
3935
|
+
});
|
|
3936
|
+
});
|
|
3937
|
+
}
|
|
3938
|
+
async function resolveCliVersionFingerprint(providerId, timeoutMs = 3e3) {
|
|
3939
|
+
const cacheKey = providerId;
|
|
3940
|
+
const cached = fingerprintCache.get(cacheKey);
|
|
3941
|
+
if (cached) {
|
|
3942
|
+
return cached;
|
|
3943
|
+
}
|
|
3944
|
+
if (fingerprintResolver) {
|
|
3945
|
+
const resolved = await fingerprintResolver(providerId);
|
|
3946
|
+
fingerprintCache.set(cacheKey, resolved);
|
|
3947
|
+
return resolved;
|
|
3948
|
+
}
|
|
3949
|
+
const binary = CLI_BINARIES[providerId];
|
|
3950
|
+
const version = await runVersionProbe(binary, timeoutMs);
|
|
3951
|
+
fingerprintCache.set(cacheKey, version);
|
|
3952
|
+
return version;
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3955
|
+
// src/core/verdict/judge-provider-matrix.ts
|
|
3956
|
+
var JUDGE_PROVIDER_SESSION_MATRIX = {
|
|
3957
|
+
cursor: {
|
|
3958
|
+
providerId: "cursor",
|
|
3959
|
+
supportsSession: true,
|
|
3960
|
+
resumeFlag: "--resume",
|
|
3961
|
+
createChatFlag: null,
|
|
3962
|
+
readOnlyArgs: ["--mode", "ask", "--sandbox", "enabled", "--trust"],
|
|
3963
|
+
rejectedUnsafeOptions: ["--force", "--yolo", "--dangerously-skip-permissions"],
|
|
3964
|
+
failureModes: [
|
|
3965
|
+
"parse_error",
|
|
3966
|
+
"non_json_response",
|
|
3967
|
+
"timeout",
|
|
3968
|
+
"cli_nonzero_exit",
|
|
3969
|
+
"io_error",
|
|
3970
|
+
"version_mismatch"
|
|
3971
|
+
],
|
|
3972
|
+
notes: "Pilot provider: ask mode + sandbox enabled; resume via --resume <chatId>."
|
|
3973
|
+
},
|
|
3974
|
+
codex: {
|
|
3975
|
+
providerId: "codex",
|
|
3976
|
+
supportsSession: false,
|
|
3977
|
+
resumeFlag: null,
|
|
3978
|
+
createChatFlag: null,
|
|
3979
|
+
readOnlyArgs: ["--sandbox", "read-only"],
|
|
3980
|
+
rejectedUnsafeOptions: ["--dangerously-bypass-approvals-and-sandbox"],
|
|
3981
|
+
failureModes: ["unsupported_resume", "parse_error", "timeout"],
|
|
3982
|
+
notes: "Session transport disabled by default; spawn read-only exec remains canonical."
|
|
3983
|
+
},
|
|
3984
|
+
claude: {
|
|
3985
|
+
providerId: "claude",
|
|
3986
|
+
supportsSession: false,
|
|
3987
|
+
resumeFlag: "--continue",
|
|
3988
|
+
createChatFlag: null,
|
|
3989
|
+
readOnlyArgs: ["--permission-mode", "plan", "--tools", "", "--bare"],
|
|
3990
|
+
rejectedUnsafeOptions: ["--dangerously-skip-permissions"],
|
|
3991
|
+
failureModes: ["unsupported_resume", "parse_error", "timeout"],
|
|
3992
|
+
notes: "Session transport gated behind allowlist after capability verification."
|
|
3993
|
+
}
|
|
3994
|
+
};
|
|
3995
|
+
function providerSupportsSession(providerId, allowlist) {
|
|
3996
|
+
const capability = JUDGE_PROVIDER_SESSION_MATRIX[providerId];
|
|
3997
|
+
return capability.supportsSession && allowlist.includes(providerId);
|
|
3998
|
+
}
|
|
3999
|
+
function assertReadOnlyInvocationArgs(providerId, args) {
|
|
4000
|
+
const capability = JUDGE_PROVIDER_SESSION_MATRIX[providerId];
|
|
4001
|
+
for (const unsafe of capability.rejectedUnsafeOptions) {
|
|
4002
|
+
if (args.includes(unsafe)) {
|
|
4003
|
+
return { ok: false, reason: "unsafe_option_rejected" };
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
for (let index = 0; index < capability.readOnlyArgs.length; index += 2) {
|
|
4007
|
+
const flag = capability.readOnlyArgs[index];
|
|
4008
|
+
const value = capability.readOnlyArgs[index + 1];
|
|
4009
|
+
if (!flag) {
|
|
4010
|
+
continue;
|
|
4011
|
+
}
|
|
4012
|
+
const flagIndex = args.indexOf(flag);
|
|
4013
|
+
if (flagIndex === -1) {
|
|
4014
|
+
return { ok: false, reason: "unsafe_option_rejected" };
|
|
4015
|
+
}
|
|
4016
|
+
if (value !== void 0 && args[flagIndex + 1] !== value) {
|
|
4017
|
+
return { ok: false, reason: "unsafe_option_rejected" };
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
return { ok: true };
|
|
4021
|
+
}
|
|
4022
|
+
|
|
4023
|
+
// src/core/verdict/judge-session-kill-switch.ts
|
|
4024
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
4025
|
+
import { mkdir as mkdir3, readFile as readFile3, unlink as unlink3, writeFile as writeFile3 } from "node:fs/promises";
|
|
4026
|
+
import path18 from "node:path";
|
|
4027
|
+
var JUDGE_SESSION_KILL_FILE = "judge-session-kill.json";
|
|
4028
|
+
function judgeSessionKillSwitchPath(stateDir) {
|
|
4029
|
+
return path18.join(stateDir, JUDGE_SESSION_KILL_FILE);
|
|
4030
|
+
}
|
|
4031
|
+
async function readJudgeSessionKillSwitch(stateDir) {
|
|
4032
|
+
const filePath = judgeSessionKillSwitchPath(stateDir);
|
|
4033
|
+
if (!existsSync6(filePath)) {
|
|
4034
|
+
return null;
|
|
4035
|
+
}
|
|
4036
|
+
try {
|
|
4037
|
+
const raw = JSON.parse(await readFile3(filePath, "utf8"));
|
|
4038
|
+
return raw.triggered === true ? raw : null;
|
|
4039
|
+
} catch {
|
|
4040
|
+
return null;
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
async function isJudgeSessionKillSwitchPersisted(stateDir) {
|
|
4044
|
+
const record = await readJudgeSessionKillSwitch(stateDir);
|
|
4045
|
+
return record?.triggered === true;
|
|
4046
|
+
}
|
|
4047
|
+
async function persistJudgeSessionKillSwitch(stateDir, reason = "shadow_mismatch") {
|
|
4048
|
+
await mkdir3(stateDir, { recursive: true, mode: 448 });
|
|
4049
|
+
const record = {
|
|
4050
|
+
triggered: true,
|
|
4051
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4052
|
+
reason
|
|
4053
|
+
};
|
|
4054
|
+
await writeFile3(judgeSessionKillSwitchPath(stateDir), JSON.stringify(record), {
|
|
4055
|
+
encoding: "utf8",
|
|
4056
|
+
mode: 384
|
|
4057
|
+
});
|
|
4058
|
+
}
|
|
4059
|
+
|
|
4060
|
+
// src/core/verdict/judge-shadow.ts
|
|
4061
|
+
var shadowStates = /* @__PURE__ */ new Map();
|
|
4062
|
+
var clock = () => /* @__PURE__ */ new Date();
|
|
4063
|
+
var random = Math.random;
|
|
4064
|
+
function dailyKeyFor(date) {
|
|
4065
|
+
return date.toISOString().slice(0, 10);
|
|
4066
|
+
}
|
|
4067
|
+
function getShadowState(repoRoot) {
|
|
4068
|
+
const existing = shadowStates.get(repoRoot);
|
|
4069
|
+
if (existing) {
|
|
4070
|
+
const key = dailyKeyFor(clock());
|
|
4071
|
+
if (existing.dailyKey !== key) {
|
|
4072
|
+
existing.dailyCount = 0;
|
|
4073
|
+
existing.dailyKey = key;
|
|
4074
|
+
}
|
|
4075
|
+
return existing;
|
|
4076
|
+
}
|
|
4077
|
+
const created = {
|
|
4078
|
+
compared: 0,
|
|
4079
|
+
mismatches: 0,
|
|
4080
|
+
dailyCount: 0,
|
|
4081
|
+
dailyKey: dailyKeyFor(clock()),
|
|
4082
|
+
killSwitchTriggered: false,
|
|
4083
|
+
mismatchWindow: []
|
|
4084
|
+
};
|
|
4085
|
+
shadowStates.set(repoRoot, created);
|
|
4086
|
+
return created;
|
|
4087
|
+
}
|
|
4088
|
+
async function isJudgeSessionKillSwitchActive(repoRoot, stateDir) {
|
|
4089
|
+
if (getShadowState(repoRoot).killSwitchTriggered) {
|
|
4090
|
+
return true;
|
|
4091
|
+
}
|
|
4092
|
+
if (stateDir) {
|
|
4093
|
+
return isJudgeSessionKillSwitchPersisted(stateDir);
|
|
4094
|
+
}
|
|
4095
|
+
return false;
|
|
4096
|
+
}
|
|
4097
|
+
async function triggerJudgeSessionKillSwitch(repoRoot, stateDir, reason = "shadow_mismatch") {
|
|
4098
|
+
getShadowState(repoRoot).killSwitchTriggered = true;
|
|
4099
|
+
if (stateDir) {
|
|
4100
|
+
await persistJudgeSessionKillSwitch(stateDir, reason);
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
function shouldRunShadowComparison(repoRoot, providerId, config) {
|
|
4104
|
+
if (!config.enabled) {
|
|
4105
|
+
return false;
|
|
4106
|
+
}
|
|
4107
|
+
if (!config.providerAllowlist.includes(providerId)) {
|
|
4108
|
+
return false;
|
|
4109
|
+
}
|
|
4110
|
+
const state = getShadowState(repoRoot);
|
|
4111
|
+
if (state.killSwitchTriggered) {
|
|
4112
|
+
return false;
|
|
4113
|
+
}
|
|
4114
|
+
if (state.dailyCount >= config.dailyRequestCap) {
|
|
4115
|
+
return false;
|
|
4116
|
+
}
|
|
4117
|
+
const rate = Math.min(config.sampleRate, config.sampleRateMax);
|
|
4118
|
+
return random() < rate;
|
|
4119
|
+
}
|
|
4120
|
+
function verdictsEquivalent(a, b) {
|
|
4121
|
+
return a.local_recoverable === b.local_recoverable && a.destroys_history_or_secrets === b.destroys_history_or_secrets;
|
|
4122
|
+
}
|
|
4123
|
+
function recordShadowComparison(repoRoot, config, mismatch) {
|
|
4124
|
+
const state = getShadowState(repoRoot);
|
|
4125
|
+
state.compared += 1;
|
|
4126
|
+
state.dailyCount += 1;
|
|
4127
|
+
if (mismatch) {
|
|
4128
|
+
state.mismatches += 1;
|
|
4129
|
+
}
|
|
4130
|
+
state.mismatchWindow.push(mismatch);
|
|
4131
|
+
if (state.mismatchWindow.length > config.windowSize) {
|
|
4132
|
+
state.mismatchWindow.shift();
|
|
4133
|
+
}
|
|
4134
|
+
const windowComparisons = state.mismatchWindow.length;
|
|
4135
|
+
const windowMismatches = state.mismatchWindow.filter(Boolean).length;
|
|
4136
|
+
const mismatchRateWindow = windowComparisons === 0 ? 0 : windowMismatches / windowComparisons;
|
|
4137
|
+
if (windowComparisons >= Math.min(10, config.windowSize) && mismatchRateWindow > config.mismatchRateThreshold) {
|
|
4138
|
+
state.killSwitchTriggered = true;
|
|
4139
|
+
}
|
|
4140
|
+
return {
|
|
4141
|
+
compared: true,
|
|
4142
|
+
mismatch,
|
|
4143
|
+
mismatchRateWindow,
|
|
4144
|
+
killSwitchTriggered: state.killSwitchTriggered
|
|
4145
|
+
};
|
|
4146
|
+
}
|
|
4147
|
+
|
|
4148
|
+
// src/core/verdict/judge-transport.ts
|
|
4149
|
+
function isEvalTimeoutError(error) {
|
|
4150
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4151
|
+
return /timed out/i.test(message);
|
|
4152
|
+
}
|
|
4153
|
+
function classifyParseFailure(providerId, raw) {
|
|
4154
|
+
const trimmed = raw.trim();
|
|
4155
|
+
if (!trimmed) {
|
|
4156
|
+
return "non_json_response";
|
|
4157
|
+
}
|
|
4158
|
+
if (parseCliJudgeOutput(providerId, trimmed)) {
|
|
4159
|
+
return "parse_error";
|
|
4160
|
+
}
|
|
4161
|
+
try {
|
|
4162
|
+
JSON.parse(trimmed);
|
|
4163
|
+
return "parse_error";
|
|
4164
|
+
} catch {
|
|
4165
|
+
return "non_json_response";
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4168
|
+
function spawnFallbackReason(providerId, error) {
|
|
4169
|
+
if (error instanceof CliRunError) {
|
|
4170
|
+
if (error.kind === "timeout") {
|
|
4171
|
+
return "eval_timeout";
|
|
4172
|
+
}
|
|
4173
|
+
if (error.kind === "exit_nonzero") {
|
|
4174
|
+
return `${providerId}_cli_nonzero`;
|
|
4175
|
+
}
|
|
4176
|
+
if (error.kind === "spawn_error") {
|
|
4177
|
+
return `${providerId}_cli_spawn_error`;
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
if (isEvalTimeoutError(error)) {
|
|
4181
|
+
return "eval_timeout";
|
|
4182
|
+
}
|
|
4183
|
+
return `${providerId}_cli_unavailable`;
|
|
4184
|
+
}
|
|
4185
|
+
function parseWithBudget(providerId, raw) {
|
|
4186
|
+
const started = Date.now();
|
|
4187
|
+
const verdict2 = parseCliJudgeOutput(providerId, raw);
|
|
4188
|
+
const parseMs = Date.now() - started;
|
|
4189
|
+
recordJudgeLatency("tier1_parse", parseMs);
|
|
4190
|
+
return { verdict: verdict2, parseMs };
|
|
4191
|
+
}
|
|
4192
|
+
async function runSpawnTransport(request, runCommand) {
|
|
4193
|
+
const { context, prompt } = request;
|
|
4194
|
+
const sessionConfig = context.runtime.session;
|
|
4195
|
+
const invocation = buildCliInvocation(context.providerId, prompt, context.model);
|
|
4196
|
+
const readOnly = assertReadOnlyInvocationArgs(context.providerId, invocation.args);
|
|
4197
|
+
if (!readOnly.ok) {
|
|
4198
|
+
return {
|
|
4199
|
+
raw: "",
|
|
4200
|
+
verdict: null,
|
|
4201
|
+
transport: "spawn",
|
|
4202
|
+
sessionUsed: false,
|
|
4203
|
+
sessionReused: false,
|
|
4204
|
+
fallbackReason: "unsafe_option_rejected",
|
|
4205
|
+
connectMs: 0,
|
|
4206
|
+
evalMs: 0,
|
|
4207
|
+
parseMs: 0
|
|
4208
|
+
};
|
|
4209
|
+
}
|
|
4210
|
+
const evalTimeoutMs = resolveSessionEvalTimeoutMs(sessionConfig, context.judgeTimeoutMs);
|
|
4211
|
+
let raw = "";
|
|
4212
|
+
let evalMs = 0;
|
|
4213
|
+
const evalStarted = Date.now();
|
|
4214
|
+
try {
|
|
4215
|
+
raw = await runCommand(invocation, evalTimeoutMs);
|
|
4216
|
+
evalMs = Date.now() - evalStarted;
|
|
4217
|
+
recordJudgeLatency("tier1_spawn", evalMs);
|
|
4218
|
+
} catch (error) {
|
|
4219
|
+
evalMs = Date.now() - evalStarted;
|
|
4220
|
+
return {
|
|
4221
|
+
raw: "",
|
|
4222
|
+
verdict: null,
|
|
4223
|
+
transport: "spawn",
|
|
4224
|
+
sessionUsed: false,
|
|
4225
|
+
sessionReused: false,
|
|
4226
|
+
fallbackReason: spawnFallbackReason(context.providerId, error),
|
|
4227
|
+
connectMs: 0,
|
|
4228
|
+
evalMs,
|
|
4229
|
+
parseMs: 0
|
|
4230
|
+
};
|
|
4231
|
+
}
|
|
4232
|
+
const parsed = parseWithBudget(context.providerId, raw);
|
|
4233
|
+
return {
|
|
4234
|
+
raw,
|
|
4235
|
+
verdict: parsed.verdict,
|
|
4236
|
+
transport: "spawn",
|
|
4237
|
+
sessionUsed: false,
|
|
4238
|
+
sessionReused: false,
|
|
4239
|
+
fallbackReason: parsed.verdict ? void 0 : classifyParseFailure(context.providerId, raw),
|
|
4240
|
+
connectMs: 0,
|
|
4241
|
+
evalMs,
|
|
4242
|
+
parseMs: parsed.parseMs
|
|
4243
|
+
};
|
|
4244
|
+
}
|
|
4245
|
+
async function runSessionTransport(request, runCommand) {
|
|
4246
|
+
const { context, prompt } = request;
|
|
4247
|
+
const sessionConfig = context.runtime.session;
|
|
4248
|
+
const connectStarted = Date.now();
|
|
4249
|
+
if (!sessionConfig.enabled || !providerSupportsSession(context.providerId, sessionConfig.providerAllowlist) || await isJudgeSessionKillSwitchActive(context.repoRoot, context.stateDir)) {
|
|
4250
|
+
return runSpawnTransport(request, runCommand);
|
|
4251
|
+
}
|
|
4252
|
+
let cliVersion = "unknown";
|
|
4253
|
+
try {
|
|
4254
|
+
cliVersion = await resolveCliVersionFingerprint(
|
|
4255
|
+
context.providerId,
|
|
4256
|
+
sessionConfig.connectTimeoutMs
|
|
4257
|
+
);
|
|
4258
|
+
} catch {
|
|
4259
|
+
return {
|
|
4260
|
+
...await runSpawnTransport(request, runCommand),
|
|
4261
|
+
fallbackReason: "connect_timeout",
|
|
4262
|
+
sessionUsed: false,
|
|
4263
|
+
sessionReused: false
|
|
4264
|
+
};
|
|
4265
|
+
}
|
|
4266
|
+
const connectMs = Date.now() - connectStarted;
|
|
4267
|
+
recordJudgeLatency("tier1_connect", connectMs);
|
|
4268
|
+
const promptBytes = Buffer.byteLength(prompt, "utf8");
|
|
4269
|
+
if (exceedsPromptBudget(promptBytes, sessionConfig)) {
|
|
4270
|
+
const spawnResult = await runSpawnTransport(request, runCommand);
|
|
4271
|
+
return {
|
|
4272
|
+
...spawnResult,
|
|
4273
|
+
sessionUsed: false,
|
|
4274
|
+
sessionReused: false,
|
|
4275
|
+
sessionResetReason: "max_prompt_bytes_exceeded",
|
|
4276
|
+
fallbackReason: "guard_reset"
|
|
4277
|
+
};
|
|
4278
|
+
}
|
|
4279
|
+
const keyParts = {
|
|
4280
|
+
providerId: context.providerId,
|
|
4281
|
+
model: context.model,
|
|
4282
|
+
repoRoot: context.repoRoot,
|
|
4283
|
+
judgeMode: context.judgeMode,
|
|
4284
|
+
cliVersion
|
|
4285
|
+
};
|
|
4286
|
+
const sessionKey = buildJudgeSessionKey(keyParts);
|
|
4287
|
+
const sessionRefHash = hashJudgeSessionRef(sessionKey);
|
|
4288
|
+
const invocation = buildCliInvocation(context.providerId, prompt, context.model);
|
|
4289
|
+
const readOnly = assertReadOnlyInvocationArgs(context.providerId, invocation.args);
|
|
4290
|
+
if (!readOnly.ok) {
|
|
4291
|
+
return runSpawnTransport(request, runCommand);
|
|
4292
|
+
}
|
|
4293
|
+
const evalTimeoutMs = resolveSessionEvalTimeoutMs(sessionConfig, context.judgeTimeoutMs);
|
|
4294
|
+
const evalStarted = Date.now();
|
|
4295
|
+
let brokerResult;
|
|
4296
|
+
try {
|
|
4297
|
+
brokerResult = await evaluateViaJudgeBroker({
|
|
4298
|
+
stateDir: context.stateDir,
|
|
4299
|
+
repoRoot: context.repoRoot,
|
|
4300
|
+
sessionConfig,
|
|
4301
|
+
request: {
|
|
4302
|
+
keyParts,
|
|
4303
|
+
invocation,
|
|
4304
|
+
promptBytes
|
|
4305
|
+
},
|
|
4306
|
+
timeoutMs: evalTimeoutMs,
|
|
4307
|
+
runCommand
|
|
4308
|
+
});
|
|
4309
|
+
} catch (error) {
|
|
4310
|
+
const spawnResult = await runSpawnTransport(request, runCommand);
|
|
4311
|
+
return {
|
|
4312
|
+
...spawnResult,
|
|
4313
|
+
transport: "session",
|
|
4314
|
+
sessionUsed: true,
|
|
4315
|
+
sessionReused: false,
|
|
4316
|
+
fallbackReason: spawnFallbackReason(context.providerId, error),
|
|
4317
|
+
sessionResetReason: "cli_error",
|
|
4318
|
+
sessionRefHash
|
|
4319
|
+
};
|
|
4320
|
+
}
|
|
4321
|
+
const evalMs = Date.now() - evalStarted;
|
|
4322
|
+
recordJudgeLatency("tier1_session", evalMs);
|
|
4323
|
+
recordJudgeLatency("tier1_eval", evalMs);
|
|
4324
|
+
const parsed = parseWithBudget(context.providerId, brokerResult.raw);
|
|
4325
|
+
if (!parsed.verdict) {
|
|
4326
|
+
const parseReason = classifyParseFailure(context.providerId, brokerResult.raw);
|
|
4327
|
+
await invalidateJudgeSession({
|
|
4328
|
+
stateDir: context.stateDir,
|
|
4329
|
+
repoRoot: context.repoRoot,
|
|
4330
|
+
sessionConfig,
|
|
4331
|
+
sessionKey,
|
|
4332
|
+
reason: parseReason === "non_json_response" ? "non_json_response" : "parse_failure"
|
|
4333
|
+
});
|
|
4334
|
+
const spawnResult = await runSpawnTransport(request, runCommand);
|
|
4335
|
+
return {
|
|
4336
|
+
...spawnResult,
|
|
4337
|
+
transport: "session",
|
|
4338
|
+
sessionUsed: true,
|
|
4339
|
+
sessionReused: brokerResult.reused,
|
|
4340
|
+
fallbackReason: guardFailClosedFallbackReason(
|
|
4341
|
+
parseReason === "non_json_response" ? "non_json_response" : "parse_failure"
|
|
4342
|
+
),
|
|
4343
|
+
sessionResetReason: parseReason === "non_json_response" ? "non_json_response" : "parse_failure",
|
|
4344
|
+
sessionRefHash
|
|
4345
|
+
};
|
|
4346
|
+
}
|
|
4347
|
+
return {
|
|
4348
|
+
raw: brokerResult.raw,
|
|
4349
|
+
verdict: parsed.verdict,
|
|
4350
|
+
transport: "session",
|
|
4351
|
+
sessionUsed: true,
|
|
4352
|
+
sessionReused: brokerResult.reused,
|
|
4353
|
+
sessionResetReason: brokerResult.resetReason,
|
|
4354
|
+
sessionRefHash,
|
|
4355
|
+
connectMs,
|
|
4356
|
+
evalMs,
|
|
4357
|
+
parseMs: parsed.parseMs
|
|
4358
|
+
};
|
|
4359
|
+
}
|
|
4360
|
+
async function evaluateWithJudgeTransport(request, options = {}) {
|
|
4361
|
+
const runCommand = options.runCommand ?? ((invocation, timeoutMs) => runCliJsonWithTimeouts(invocation, { evalTimeoutMs: timeoutMs }));
|
|
4362
|
+
const { context } = request;
|
|
4363
|
+
const useSession = context.runtime.session.enabled && providerSupportsSession(context.providerId, context.runtime.session.providerAllowlist) && !await isJudgeSessionKillSwitchActive(context.repoRoot, context.stateDir);
|
|
4364
|
+
const primary = useSession ? await runSessionTransport(request, runCommand) : await runSpawnTransport(request, runCommand);
|
|
4365
|
+
if (!shouldRunShadowComparison(context.repoRoot, context.providerId, context.runtime.shadow)) {
|
|
4366
|
+
return primary;
|
|
4367
|
+
}
|
|
4368
|
+
const shadowSpawn = primary.transport === "session" ? await runSpawnTransport(request, runCommand) : primary;
|
|
4369
|
+
if (!primary.verdict || !shadowSpawn.verdict) {
|
|
4370
|
+
return primary;
|
|
4371
|
+
}
|
|
4372
|
+
const shadow = recordShadowComparison(
|
|
4373
|
+
context.repoRoot,
|
|
4374
|
+
context.runtime.shadow,
|
|
4375
|
+
!verdictsEquivalent(primary.verdict, shadowSpawn.verdict)
|
|
4376
|
+
);
|
|
4377
|
+
if (shadow.killSwitchTriggered) {
|
|
4378
|
+
await triggerJudgeSessionKillSwitch(context.repoRoot, context.stateDir);
|
|
4379
|
+
}
|
|
4380
|
+
return {
|
|
4381
|
+
...primary,
|
|
4382
|
+
shadowCompared: shadow.compared,
|
|
4383
|
+
shadowMismatch: shadow.mismatch,
|
|
4384
|
+
shadowMismatchRateWindow: shadow.mismatchRateWindow,
|
|
4385
|
+
killSwitchTriggered: shadow.killSwitchTriggered,
|
|
4386
|
+
verdict: shadow.mismatch ? shadowSpawn.verdict : primary.verdict,
|
|
4387
|
+
fallbackReason: shadow.mismatch ? "shadow_forced_spawn" : primary.fallbackReason,
|
|
4388
|
+
transport: shadow.mismatch ? "spawn" : primary.transport,
|
|
4389
|
+
sessionUsed: shadow.mismatch ? false : primary.sessionUsed,
|
|
4390
|
+
sessionReused: shadow.mismatch ? false : primary.sessionReused
|
|
4391
|
+
};
|
|
2612
4392
|
}
|
|
2613
4393
|
|
|
2614
4394
|
// src/core/verdict/judge-cli.ts
|
|
2615
|
-
|
|
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,26 +8503,44 @@ 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 gateUnmappedToolVerdict(ctx, deps, toolName, payload) {
|
|
6250
|
-
const
|
|
8537
|
+
const scrubOpts = scrubOptionsFromConfig(ctx.config);
|
|
8538
|
+
const replayPayload = fingerprintReplayPayload("tool", payload, scrubOpts) ?? {};
|
|
6251
8539
|
const result = {
|
|
6252
8540
|
verdict: "deny_pending_approval",
|
|
6253
8541
|
reason: "unmapped_tool",
|
|
6254
8542
|
summary: toolName,
|
|
6255
|
-
fingerprint: toolFingerprint(toolName,
|
|
8543
|
+
fingerprint: toolFingerprint(toolName, replayPayload, ctx.repoRoot),
|
|
6256
8544
|
assessment: {
|
|
6257
8545
|
reversibility: "irreversible",
|
|
6258
8546
|
external: false,
|
|
@@ -6264,11 +8552,24 @@ async function gateUnmappedToolVerdict(ctx, deps, toolName, payload) {
|
|
|
6264
8552
|
return gateDecisionToVerdict(ctx, deps, "tool", result, {
|
|
6265
8553
|
approvalInput: {
|
|
6266
8554
|
input: toolName,
|
|
6267
|
-
inputKind: "tool"
|
|
8555
|
+
inputKind: "tool",
|
|
8556
|
+
cwd: ctx.repoRoot,
|
|
8557
|
+
toolName,
|
|
8558
|
+
payload: replayPayload
|
|
8559
|
+
},
|
|
8560
|
+
replayAction: {
|
|
8561
|
+
kind: "tool",
|
|
8562
|
+
cwd: ctx.repoRoot,
|
|
8563
|
+
toolName,
|
|
8564
|
+
payload: replayPayload,
|
|
8565
|
+
fingerprint: result.fingerprint,
|
|
8566
|
+
repoRoot: ctx.repoRoot
|
|
6268
8567
|
}
|
|
6269
8568
|
});
|
|
6270
8569
|
}
|
|
6271
8570
|
async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {}) {
|
|
8571
|
+
const replayContext = buildAuditReplayContext(kind, result, auditExtras.replayAction);
|
|
8572
|
+
const actionSnapshot = buildAuditActionSnapshot(kind, result, auditExtras.replayAction);
|
|
6272
8573
|
const gateBase = {
|
|
6273
8574
|
event: gateAuditEventName(kind),
|
|
6274
8575
|
kind,
|
|
@@ -6280,7 +8581,9 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
6280
8581
|
mode: ctx.config.mode,
|
|
6281
8582
|
schemaVersion: result.axes ? 2 : 1,
|
|
6282
8583
|
...result.axes ?? {},
|
|
6283
|
-
...auditExtras.transactionalLayer
|
|
8584
|
+
...auditExtras.transactionalLayer,
|
|
8585
|
+
...replayContext ? { replayContext } : {},
|
|
8586
|
+
...actionSnapshot ? { actionSnapshot } : {}
|
|
6284
8587
|
};
|
|
6285
8588
|
if (result.reason === TRANSACTIONAL_ALREADY_APPLIED) {
|
|
6286
8589
|
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.";
|
|
@@ -6301,6 +8604,36 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
6301
8604
|
agent_message: agentMessage
|
|
6302
8605
|
});
|
|
6303
8606
|
}
|
|
8607
|
+
if (result.verdict === "deny_pending_approval" && ctx.config.mode === "enforce" && !TRANSACTIONAL_APPROVAL_BYPASS_REASONS.has(result.reason)) {
|
|
8608
|
+
const standingAllowPath = standingAllowFile(
|
|
8609
|
+
ctx.config,
|
|
8610
|
+
ctx.layout.repoLocalStateDir(ctx.repoRoot)
|
|
8611
|
+
);
|
|
8612
|
+
const standingState = await loadStandingAllow(standingAllowPath);
|
|
8613
|
+
const standingMatch = resolveStandingAllowMatch({
|
|
8614
|
+
kind,
|
|
8615
|
+
result,
|
|
8616
|
+
repoRoot: ctx.repoRoot,
|
|
8617
|
+
state: standingState
|
|
8618
|
+
});
|
|
8619
|
+
if (standingMatch) {
|
|
8620
|
+
await deps.appendAudit(ctx, {
|
|
8621
|
+
...gateBase,
|
|
8622
|
+
verdict: "allow",
|
|
8623
|
+
reason: "standing_allow",
|
|
8624
|
+
standingAllowSource: standingMatch.source,
|
|
8625
|
+
...standingMatch.catalogCommand ? { standingAllowCatalogCommand: standingMatch.catalogCommand } : {},
|
|
8626
|
+
wouldBlock: false,
|
|
8627
|
+
permission: "allow"
|
|
8628
|
+
});
|
|
8629
|
+
return classifyResultToGateVerdict({
|
|
8630
|
+
result: { ...result, verdict: "allow", reason: "standing_allow" },
|
|
8631
|
+
mode: ctx.config.mode,
|
|
8632
|
+
permission: "allow",
|
|
8633
|
+
wouldBlock: false
|
|
8634
|
+
});
|
|
8635
|
+
}
|
|
8636
|
+
}
|
|
6304
8637
|
const brokerActive = isCapabilityBrokerDemotionActive(ctx.config);
|
|
6305
8638
|
let approved = null;
|
|
6306
8639
|
if (!TRANSACTIONAL_APPROVAL_BYPASS_REASONS.has(result.reason) && !shouldSkipBrokerApprovedOnce(brokerActive, result.reason)) {
|
|
@@ -6310,6 +8643,29 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
6310
8643
|
(entry) => entry.kind === kind && entry.fingerprint === result.fingerprint && entry.repoRoot === ctx.repoRoot
|
|
6311
8644
|
);
|
|
6312
8645
|
if (!shouldSkipBrokerApprovedRecord(brokerActive, matchedApproval?.reason)) {
|
|
8646
|
+
if (matchedApproval && auditExtras.replayAction && !validateReplayEnvelope(matchedApproval, auditExtras.replayAction)) {
|
|
8647
|
+
await deps.appendAudit(ctx, {
|
|
8648
|
+
...gateBase,
|
|
8649
|
+
verdict: "deny_pending_approval",
|
|
8650
|
+
reason: "approval_replay_mismatch",
|
|
8651
|
+
approvalId: matchedApproval.approvalId,
|
|
8652
|
+
wouldBlock: true,
|
|
8653
|
+
permission: "deny"
|
|
8654
|
+
});
|
|
8655
|
+
return classifyResultToGateVerdict({
|
|
8656
|
+
result: {
|
|
8657
|
+
...result,
|
|
8658
|
+
verdict: "deny_pending_approval",
|
|
8659
|
+
reason: "approval_replay_mismatch"
|
|
8660
|
+
},
|
|
8661
|
+
mode: ctx.config.mode,
|
|
8662
|
+
permission: "deny",
|
|
8663
|
+
wouldBlock: true,
|
|
8664
|
+
approvalId: matchedApproval.approvalId,
|
|
8665
|
+
user_message: "Belay denied this action because it does not match the approved replay envelope. Re-approve the exact action or run belay explain.",
|
|
8666
|
+
agent_message: "Belay denied this action because cwd, tool, or payload changed after approval."
|
|
8667
|
+
});
|
|
8668
|
+
}
|
|
6313
8669
|
approved = await consumeApprovedApproval(ctx, deps, kind, result.fingerprint);
|
|
6314
8670
|
}
|
|
6315
8671
|
}
|
|
@@ -6360,7 +8716,19 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
6360
8716
|
wouldBlock: true
|
|
6361
8717
|
});
|
|
6362
8718
|
}
|
|
6363
|
-
const approval = await ensurePendingApproval(
|
|
8719
|
+
const approval = await ensurePendingApproval(
|
|
8720
|
+
ctx,
|
|
8721
|
+
deps,
|
|
8722
|
+
kind,
|
|
8723
|
+
result,
|
|
8724
|
+
auditExtras.approvalInput,
|
|
8725
|
+
deriveWorkspaceRootScopeHint({
|
|
8726
|
+
result,
|
|
8727
|
+
replayAction: auditExtras.replayAction,
|
|
8728
|
+
payloadForScopeHint: auditExtras.scopeHintPayload,
|
|
8729
|
+
options: auditExtras.classifierOptions ?? {}
|
|
8730
|
+
})
|
|
8731
|
+
);
|
|
6364
8732
|
let approvalToken;
|
|
6365
8733
|
try {
|
|
6366
8734
|
approvalToken = await issueApprovalToken(
|
|
@@ -6400,7 +8768,7 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
6400
8768
|
permission: "deny",
|
|
6401
8769
|
wouldBlock: true,
|
|
6402
8770
|
approvalId: approval.approvalId,
|
|
6403
|
-
user_message: `Belay blocked this high-risk action. Approval ID: ${approval.approvalId}. ${
|
|
8771
|
+
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.`,
|
|
6404
8772
|
agent_message: `Belay denied this action as ${result.reason}. Wait for approval, then retry the exact same action once.`
|
|
6405
8773
|
});
|
|
6406
8774
|
}
|
|
@@ -6409,6 +8777,7 @@ async function processApprovalPrompt(ctx, deps, prompt) {
|
|
|
6409
8777
|
if (!approvalId) {
|
|
6410
8778
|
return { continue: true };
|
|
6411
8779
|
}
|
|
8780
|
+
const adapter = adapterIdFromContext(ctx);
|
|
6412
8781
|
if (ctx.config.approvalSigning.required) {
|
|
6413
8782
|
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>.`;
|
|
6414
8783
|
await deps.appendAudit(ctx, {
|
|
@@ -6428,12 +8797,11 @@ async function processApprovalPrompt(ctx, deps, prompt) {
|
|
|
6428
8797
|
approvalId,
|
|
6429
8798
|
config: ctx.config,
|
|
6430
8799
|
requireSignedToken: ctx.config.approvalSigning.required,
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
}
|
|
8800
|
+
adapter,
|
|
8801
|
+
store: gateApprovalStoreFromDeps({
|
|
8802
|
+
loadApprovals: (fileName) => deps.loadApprovals(ctx, fileName),
|
|
8803
|
+
writeApprovals: (filePath, state) => deps.writeApprovals(filePath, state)
|
|
8804
|
+
})
|
|
6437
8805
|
});
|
|
6438
8806
|
await deps.appendAudit(ctx, {
|
|
6439
8807
|
event: "approval",
|
|
@@ -6449,9 +8817,91 @@ async function processApprovalPrompt(ctx, deps, prompt) {
|
|
|
6449
8817
|
user_message: recorded.message
|
|
6450
8818
|
};
|
|
6451
8819
|
}
|
|
8820
|
+
const replay = recorded.approval ? buildReplayHint(ctx.config, recorded.approval, adapter) : null;
|
|
8821
|
+
const shouldFallbackReplay = adapter === "cursor" && replay?.kind === "shell" && replay.autoReplay === true && Boolean(recorded.approval?.input) && (!process.env.VITEST || process.env.BELAY_TEST_APPROVAL_REPLAY === "1");
|
|
8822
|
+
if (shouldFallbackReplay && recorded.approval) {
|
|
8823
|
+
const approvalStore = gateApprovalStoreFromDeps({
|
|
8824
|
+
loadApprovals: (fileName) => deps.loadApprovals(ctx, fileName),
|
|
8825
|
+
writeApprovals: (filePath, state) => deps.writeApprovals(filePath, state)
|
|
8826
|
+
});
|
|
8827
|
+
let replayResult;
|
|
8828
|
+
try {
|
|
8829
|
+
replayResult = await replayShellCommand(
|
|
8830
|
+
recorded.approval.input ?? "",
|
|
8831
|
+
recorded.approval.cwd ?? ctx.repoRoot,
|
|
8832
|
+
getExecutionLeaseMs(ctx.config)
|
|
8833
|
+
);
|
|
8834
|
+
} catch (error) {
|
|
8835
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
8836
|
+
await deps.appendAudit(ctx, {
|
|
8837
|
+
event: "approval",
|
|
8838
|
+
kind: "approval",
|
|
8839
|
+
verdict: "allow",
|
|
8840
|
+
approvalId,
|
|
8841
|
+
reason: "approval_replay_error",
|
|
8842
|
+
summary: recorded.approval.input ?? recorded.approval.summary ?? prompt
|
|
8843
|
+
});
|
|
8844
|
+
return {
|
|
8845
|
+
continue: false,
|
|
8846
|
+
user_message: `Belay approval recorded for ${approvalId}, but one-step shell replay could not start (${detail}). Approval remains active for one hook retry.`
|
|
8847
|
+
};
|
|
8848
|
+
}
|
|
8849
|
+
const output = [replayResult.stdout, replayResult.stderr].filter(Boolean).join("\n").trim();
|
|
8850
|
+
if (replayResult.exitCode === 0) {
|
|
8851
|
+
try {
|
|
8852
|
+
await consumeApprovedAfterCliReplay({
|
|
8853
|
+
approvalId,
|
|
8854
|
+
store: approvalStore
|
|
8855
|
+
});
|
|
8856
|
+
} catch (error) {
|
|
8857
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
8858
|
+
await deps.appendAudit(ctx, {
|
|
8859
|
+
event: "approval",
|
|
8860
|
+
kind: "approval",
|
|
8861
|
+
verdict: "allow",
|
|
8862
|
+
approvalId,
|
|
8863
|
+
reason: "approval_replay_consume_error",
|
|
8864
|
+
summary: recorded.approval.input ?? recorded.approval.summary ?? prompt
|
|
8865
|
+
});
|
|
8866
|
+
return {
|
|
8867
|
+
continue: false,
|
|
8868
|
+
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 ? `
|
|
8869
|
+
${output}` : "")
|
|
8870
|
+
};
|
|
8871
|
+
}
|
|
8872
|
+
await deps.appendAudit(ctx, {
|
|
8873
|
+
event: "approval",
|
|
8874
|
+
kind: "approval",
|
|
8875
|
+
verdict: "allow",
|
|
8876
|
+
approvalId,
|
|
8877
|
+
reason: "approval_replay_succeeded",
|
|
8878
|
+
summary: recorded.approval.input ?? recorded.approval.summary ?? prompt
|
|
8879
|
+
});
|
|
8880
|
+
return {
|
|
8881
|
+
continue: false,
|
|
8882
|
+
user_message: `Belay approval recorded for ${approvalId}. One-step shell replay succeeded; no manual retry required.` + (output ? `
|
|
8883
|
+
${output}` : "")
|
|
8884
|
+
};
|
|
8885
|
+
}
|
|
8886
|
+
await deps.appendAudit(ctx, {
|
|
8887
|
+
event: "approval",
|
|
8888
|
+
kind: "approval",
|
|
8889
|
+
verdict: "allow",
|
|
8890
|
+
approvalId,
|
|
8891
|
+
reason: "approval_replay_failed",
|
|
8892
|
+
summary: recorded.approval.input ?? recorded.approval.summary ?? prompt
|
|
8893
|
+
});
|
|
8894
|
+
const timeoutNote = replayResult.timedOut ? " Replay timed out." : "";
|
|
8895
|
+
return {
|
|
8896
|
+
continue: false,
|
|
8897
|
+
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 ? `
|
|
8898
|
+
${output}` : "")
|
|
8899
|
+
};
|
|
8900
|
+
}
|
|
6452
8901
|
return {
|
|
6453
8902
|
continue: false,
|
|
6454
|
-
user_message: recorded.message
|
|
8903
|
+
user_message: recorded.message,
|
|
8904
|
+
...replay ? { replay } : {}
|
|
6455
8905
|
};
|
|
6456
8906
|
}
|
|
6457
8907
|
function gateVerdictToClaudePreToolUseResponse(verdict2) {
|
|
@@ -6475,7 +8925,8 @@ function gateVerdictToCodexUserPromptResponse(verdict2) {
|
|
|
6475
8925
|
}
|
|
6476
8926
|
return {
|
|
6477
8927
|
decision: "block",
|
|
6478
|
-
reason: verdict2.user_message
|
|
8928
|
+
reason: verdict2.user_message,
|
|
8929
|
+
...verdict2.replay ? { replay: verdict2.replay } : {}
|
|
6479
8930
|
};
|
|
6480
8931
|
}
|
|
6481
8932
|
async function appendObservedAudit(ctx, deps, eventName, payload) {
|
|
@@ -6489,19 +8940,19 @@ async function appendObservedAudit(ctx, deps, eventName, payload) {
|
|
|
6489
8940
|
}
|
|
6490
8941
|
|
|
6491
8942
|
// src/adapters/shared/repo-root.ts
|
|
6492
|
-
import { existsSync as
|
|
6493
|
-
import
|
|
8943
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
8944
|
+
import path27 from "node:path";
|
|
6494
8945
|
function findRepoRoot(startPath, layout) {
|
|
6495
|
-
let current =
|
|
8946
|
+
let current = path27.resolve(startPath);
|
|
6496
8947
|
while (true) {
|
|
6497
8948
|
for (const marker of layout.repoRootMarkers) {
|
|
6498
|
-
if (
|
|
8949
|
+
if (existsSync11(path27.join(current, marker))) {
|
|
6499
8950
|
return current;
|
|
6500
8951
|
}
|
|
6501
8952
|
}
|
|
6502
|
-
const parent =
|
|
8953
|
+
const parent = path27.dirname(current);
|
|
6503
8954
|
if (parent === current) {
|
|
6504
|
-
return
|
|
8955
|
+
return path27.resolve(startPath);
|
|
6505
8956
|
}
|
|
6506
8957
|
current = parent;
|
|
6507
8958
|
}
|