@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
|
@@ -196,6 +196,115 @@ function isCloudProviderId(providerId) {
|
|
|
196
196
|
return JUDGE_CATALOG[normalized].isCloud;
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
// src/core/verdict/judge-runtime-config.ts
|
|
200
|
+
var DEFAULT_JUDGE_SESSION_CONFIG = {
|
|
201
|
+
enabled: false,
|
|
202
|
+
maxTurns: 32,
|
|
203
|
+
maxAgeMs: 30 * 60 * 1e3,
|
|
204
|
+
maxIdleMs: 5 * 60 * 1e3,
|
|
205
|
+
maxPromptBytes: 64 * 1024,
|
|
206
|
+
providerAllowlist: ["cursor"],
|
|
207
|
+
connectTimeoutMs: 5e3,
|
|
208
|
+
evalTimeoutMs: null,
|
|
209
|
+
parseTimeoutMs: 2e3,
|
|
210
|
+
maxSessionsPerProvider: 1
|
|
211
|
+
};
|
|
212
|
+
var DEFAULT_JUDGE_SHADOW_CONFIG = {
|
|
213
|
+
enabled: false,
|
|
214
|
+
sampleRate: 0.01,
|
|
215
|
+
sampleRateMax: 0.05,
|
|
216
|
+
dailyRequestCap: 500,
|
|
217
|
+
mismatchRateThreshold: 0.02,
|
|
218
|
+
providerAllowlist: ["cursor"],
|
|
219
|
+
windowSize: 100
|
|
220
|
+
};
|
|
221
|
+
var DEFAULT_JUDGE_RUNTIME_CONFIG = {
|
|
222
|
+
session: { ...DEFAULT_JUDGE_SESSION_CONFIG },
|
|
223
|
+
shadow: { ...DEFAULT_JUDGE_SHADOW_CONFIG }
|
|
224
|
+
};
|
|
225
|
+
var CLI_PROVIDER_IDS = ["codex", "cursor", "claude"];
|
|
226
|
+
function normalizeProviderAllowlist(value, fallback) {
|
|
227
|
+
if (!Array.isArray(value)) {
|
|
228
|
+
return [...fallback];
|
|
229
|
+
}
|
|
230
|
+
const allowed = value.map((entry) => String(entry).trim()).filter(
|
|
231
|
+
(entry) => CLI_PROVIDER_IDS.includes(entry)
|
|
232
|
+
);
|
|
233
|
+
return allowed.length > 0 ? [...new Set(allowed)] : [...fallback];
|
|
234
|
+
}
|
|
235
|
+
function normalizePositiveInt(value, fallback) {
|
|
236
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
237
|
+
}
|
|
238
|
+
function normalizeRate(value, fallback, max) {
|
|
239
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
240
|
+
return fallback;
|
|
241
|
+
}
|
|
242
|
+
return Math.min(value, max);
|
|
243
|
+
}
|
|
244
|
+
function normalizeJudgeRuntimeConfig(runtime) {
|
|
245
|
+
const session = runtime?.session ?? {};
|
|
246
|
+
const shadow = runtime?.shadow ?? {};
|
|
247
|
+
return {
|
|
248
|
+
session: normalizeJudgeSessionConfig(session),
|
|
249
|
+
shadow: {
|
|
250
|
+
enabled: shadow.enabled === true,
|
|
251
|
+
sampleRate: normalizeRate(shadow.sampleRate, DEFAULT_JUDGE_SHADOW_CONFIG.sampleRate, 1),
|
|
252
|
+
sampleRateMax: normalizeRate(
|
|
253
|
+
shadow.sampleRateMax,
|
|
254
|
+
DEFAULT_JUDGE_SHADOW_CONFIG.sampleRateMax,
|
|
255
|
+
1
|
|
256
|
+
),
|
|
257
|
+
dailyRequestCap: normalizePositiveInt(
|
|
258
|
+
shadow.dailyRequestCap,
|
|
259
|
+
DEFAULT_JUDGE_SHADOW_CONFIG.dailyRequestCap
|
|
260
|
+
),
|
|
261
|
+
mismatchRateThreshold: normalizeRate(
|
|
262
|
+
shadow.mismatchRateThreshold,
|
|
263
|
+
DEFAULT_JUDGE_SHADOW_CONFIG.mismatchRateThreshold,
|
|
264
|
+
1
|
|
265
|
+
),
|
|
266
|
+
providerAllowlist: normalizeProviderAllowlist(
|
|
267
|
+
shadow.providerAllowlist,
|
|
268
|
+
DEFAULT_JUDGE_SHADOW_CONFIG.providerAllowlist
|
|
269
|
+
),
|
|
270
|
+
windowSize: normalizePositiveInt(shadow.windowSize, DEFAULT_JUDGE_SHADOW_CONFIG.windowSize)
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function normalizeJudgeSessionConfig(session) {
|
|
275
|
+
const input = session ?? {};
|
|
276
|
+
return {
|
|
277
|
+
enabled: input.enabled === true,
|
|
278
|
+
maxTurns: normalizePositiveInt(input.maxTurns, DEFAULT_JUDGE_SESSION_CONFIG.maxTurns),
|
|
279
|
+
maxAgeMs: normalizePositiveInt(input.maxAgeMs, DEFAULT_JUDGE_SESSION_CONFIG.maxAgeMs),
|
|
280
|
+
maxIdleMs: normalizePositiveInt(input.maxIdleMs, DEFAULT_JUDGE_SESSION_CONFIG.maxIdleMs),
|
|
281
|
+
maxPromptBytes: normalizePositiveInt(
|
|
282
|
+
input.maxPromptBytes,
|
|
283
|
+
DEFAULT_JUDGE_SESSION_CONFIG.maxPromptBytes
|
|
284
|
+
),
|
|
285
|
+
providerAllowlist: normalizeProviderAllowlist(
|
|
286
|
+
input.providerAllowlist,
|
|
287
|
+
DEFAULT_JUDGE_SESSION_CONFIG.providerAllowlist
|
|
288
|
+
),
|
|
289
|
+
connectTimeoutMs: normalizePositiveInt(
|
|
290
|
+
input.connectTimeoutMs,
|
|
291
|
+
DEFAULT_JUDGE_SESSION_CONFIG.connectTimeoutMs
|
|
292
|
+
),
|
|
293
|
+
evalTimeoutMs: typeof input.evalTimeoutMs === "number" && input.evalTimeoutMs > 0 ? Math.floor(input.evalTimeoutMs) : null,
|
|
294
|
+
parseTimeoutMs: normalizePositiveInt(
|
|
295
|
+
input.parseTimeoutMs,
|
|
296
|
+
DEFAULT_JUDGE_SESSION_CONFIG.parseTimeoutMs
|
|
297
|
+
),
|
|
298
|
+
maxSessionsPerProvider: normalizePositiveInt(
|
|
299
|
+
input.maxSessionsPerProvider,
|
|
300
|
+
DEFAULT_JUDGE_SESSION_CONFIG.maxSessionsPerProvider
|
|
301
|
+
)
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function resolveSessionEvalTimeoutMs(sessionConfig, judgeTimeoutMs) {
|
|
305
|
+
return sessionConfig.evalTimeoutMs ?? judgeTimeoutMs;
|
|
306
|
+
}
|
|
307
|
+
|
|
199
308
|
// src/core/audit-summary.ts
|
|
200
309
|
var DEFAULT_SILENT_PASS_THRESHOLD = 0.5;
|
|
201
310
|
|
|
@@ -288,6 +397,15 @@ var DEFAULT_NOTIFICATIONS_V3 = {};
|
|
|
288
397
|
var DEFAULT_APPROVAL_SIGNING_V3 = {
|
|
289
398
|
required: false
|
|
290
399
|
};
|
|
400
|
+
var DEFAULT_APPROVAL_CONFIG = {
|
|
401
|
+
flow: "one_step",
|
|
402
|
+
autoReplayScopes: {
|
|
403
|
+
shell: true,
|
|
404
|
+
tool: false,
|
|
405
|
+
subagent: false
|
|
406
|
+
},
|
|
407
|
+
executionLeaseMs: 6e4
|
|
408
|
+
};
|
|
291
409
|
var DEFAULT_EGRESS_V3 = {
|
|
292
410
|
enabled: false,
|
|
293
411
|
listenHost: "127.0.0.1",
|
|
@@ -341,6 +459,7 @@ var DEFAULT_CONFIG_V4 = {
|
|
|
341
459
|
controlPlane: { ...DEFAULT_CONTROL_PLANE_V3 },
|
|
342
460
|
notifications: { ...DEFAULT_NOTIFICATIONS_V3 },
|
|
343
461
|
approvalSigning: { ...DEFAULT_APPROVAL_SIGNING_V3 },
|
|
462
|
+
approval: { ...DEFAULT_APPROVAL_CONFIG },
|
|
344
463
|
egress: { ...DEFAULT_EGRESS_V3 },
|
|
345
464
|
sandbox: { ...DEFAULT_SANDBOX_V3 },
|
|
346
465
|
audit: { ...DEFAULT_CONFIG_V2.audit },
|
|
@@ -380,6 +499,7 @@ function migrateV2ToV3(v2, rawOverrides) {
|
|
|
380
499
|
controlPlane: { ...LEGACY_CONTROL_PLANE_V3 },
|
|
381
500
|
notifications: { ...DEFAULT_NOTIFICATIONS_V3 },
|
|
382
501
|
approvalSigning: { ...DEFAULT_APPROVAL_SIGNING_V3 },
|
|
502
|
+
approval: { ...DEFAULT_APPROVAL_CONFIG },
|
|
383
503
|
egress: { ...DEFAULT_EGRESS_V3 },
|
|
384
504
|
sandbox: { ...DEFAULT_SANDBOX_V3 },
|
|
385
505
|
audit: v2.audit,
|
|
@@ -448,7 +568,8 @@ function normalizeJudgeConfig(judge) {
|
|
|
448
568
|
endpoint: endpoint2,
|
|
449
569
|
keepAlive: provider === "ollama" && typeof judge.keepAlive === "string" && judge.keepAlive.trim() ? judge.keepAlive.trim() : provider === "ollama" ? DEFAULT_JUDGE_LOCAL_OLLAMA.keepAlive : null,
|
|
450
570
|
...judge.cloudConsent ? { cloudConsent: judge.cloudConsent } : {},
|
|
451
|
-
...judge.credential ? { credential: judge.credential } : {}
|
|
571
|
+
...judge.credential ? { credential: judge.credential } : {},
|
|
572
|
+
runtime: normalizeJudgeRuntimeConfig(judge.runtime)
|
|
452
573
|
};
|
|
453
574
|
}
|
|
454
575
|
let providerId = rawProviderId && normalizeLegacyProviderId(rawProviderId) ? normalizeLegacyProviderId(rawProviderId) : inferProviderIdFromConfig({ ...judge, provider });
|
|
@@ -493,6 +614,7 @@ function normalizeJudgeConfig(judge) {
|
|
|
493
614
|
...judge.credential.ref ? { ref: judge.credential.ref } : {}
|
|
494
615
|
};
|
|
495
616
|
}
|
|
617
|
+
normalized.runtime = normalizeJudgeRuntimeConfig(judge.runtime);
|
|
496
618
|
return normalized;
|
|
497
619
|
}
|
|
498
620
|
function rejectTeamLayerJudgeSecrets(judge, source) {
|
|
@@ -541,6 +663,14 @@ function mergeV3FromRaw(base, raw) {
|
|
|
541
663
|
...base.approvalSigning,
|
|
542
664
|
...raw.approvalSigning ?? {}
|
|
543
665
|
},
|
|
666
|
+
approval: {
|
|
667
|
+
...base.approval,
|
|
668
|
+
...raw.approval ?? {},
|
|
669
|
+
autoReplayScopes: {
|
|
670
|
+
...base.approval.autoReplayScopes,
|
|
671
|
+
...raw.approval?.autoReplayScopes ?? {}
|
|
672
|
+
}
|
|
673
|
+
},
|
|
544
674
|
egress: {
|
|
545
675
|
...base.egress,
|
|
546
676
|
...raw.egress ?? {}
|
|
@@ -592,6 +722,14 @@ function normalizeV3Raw(raw) {
|
|
|
592
722
|
approvalSigning: {
|
|
593
723
|
required: raw.approvalSigning?.required === true
|
|
594
724
|
},
|
|
725
|
+
approval: {
|
|
726
|
+
...DEFAULT_APPROVAL_CONFIG,
|
|
727
|
+
...raw.approval ?? {},
|
|
728
|
+
autoReplayScopes: {
|
|
729
|
+
...DEFAULT_APPROVAL_CONFIG.autoReplayScopes,
|
|
730
|
+
...raw.approval?.autoReplayScopes ?? {}
|
|
731
|
+
}
|
|
732
|
+
},
|
|
595
733
|
egress: {
|
|
596
734
|
...DEFAULT_EGRESS_V3,
|
|
597
735
|
...raw.egress ?? {}
|
|
@@ -761,6 +899,15 @@ function normalizeConfig(config) {
|
|
|
761
899
|
approvalSigning: {
|
|
762
900
|
required: v4.approvalSigning?.required === true
|
|
763
901
|
},
|
|
902
|
+
approval: {
|
|
903
|
+
flow: v4.approval?.flow === "two_step" ? "two_step" : DEFAULT_APPROVAL_CONFIG.flow,
|
|
904
|
+
autoReplayScopes: {
|
|
905
|
+
shell: v4.approval?.autoReplayScopes?.shell !== false,
|
|
906
|
+
tool: v4.approval?.autoReplayScopes?.tool === true,
|
|
907
|
+
subagent: v4.approval?.autoReplayScopes?.subagent === true
|
|
908
|
+
},
|
|
909
|
+
executionLeaseMs: typeof v4.approval?.executionLeaseMs === "number" && v4.approval.executionLeaseMs > 0 ? v4.approval.executionLeaseMs : DEFAULT_APPROVAL_CONFIG.executionLeaseMs
|
|
910
|
+
},
|
|
764
911
|
egress: {
|
|
765
912
|
enabled: v4.egress?.enabled === true,
|
|
766
913
|
listenHost: normalizeEgressListenHost(
|
|
@@ -828,6 +975,14 @@ function mergeConfig(existing, defaults = DEFAULT_CONFIG_V4) {
|
|
|
828
975
|
...defaults.approvalSigning,
|
|
829
976
|
...migrated.approvalSigning
|
|
830
977
|
},
|
|
978
|
+
approval: {
|
|
979
|
+
...defaults.approval,
|
|
980
|
+
...migrated.approval,
|
|
981
|
+
autoReplayScopes: {
|
|
982
|
+
...defaults.approval.autoReplayScopes,
|
|
983
|
+
...migrated.approval.autoReplayScopes
|
|
984
|
+
}
|
|
985
|
+
},
|
|
831
986
|
egress: {
|
|
832
987
|
...defaults.egress,
|
|
833
988
|
...migrated.egress
|
|
@@ -945,12 +1100,216 @@ var claudeLayout = {
|
|
|
945
1100
|
|
|
946
1101
|
// src/adapters/shared/gate-runtime.ts
|
|
947
1102
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
948
|
-
import { existsSync as
|
|
949
|
-
import { mkdir as
|
|
950
|
-
import
|
|
1103
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
1104
|
+
import { mkdir as mkdir6, readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
|
|
1105
|
+
import path26 from "node:path";
|
|
1106
|
+
|
|
1107
|
+
// src/core/approval-replay.ts
|
|
1108
|
+
import path5 from "node:path";
|
|
1109
|
+
|
|
1110
|
+
// src/core/fingerprint.ts
|
|
1111
|
+
import { createHash } from "node:crypto";
|
|
1112
|
+
function canonicalStringify(value) {
|
|
1113
|
+
if (value === null || typeof value !== "object") {
|
|
1114
|
+
return JSON.stringify(value);
|
|
1115
|
+
}
|
|
1116
|
+
if (Array.isArray(value)) {
|
|
1117
|
+
return `[${value.map((item) => canonicalStringify(item)).join(",")}]`;
|
|
1118
|
+
}
|
|
1119
|
+
const entries = Object.entries(value).sort(
|
|
1120
|
+
([left], [right]) => left.localeCompare(right)
|
|
1121
|
+
);
|
|
1122
|
+
return `{${entries.map(([key, child]) => `${JSON.stringify(key)}:${canonicalStringify(child)}`).join(",")}}`;
|
|
1123
|
+
}
|
|
1124
|
+
function hashValue(value) {
|
|
1125
|
+
return createHash("sha256").update(value).digest("hex");
|
|
1126
|
+
}
|
|
1127
|
+
function subagentFingerprint(kind, scrubbed, repoRoot) {
|
|
1128
|
+
return hashValue(`subagent:${kind}:${canonicalStringify(scrubbed)}:${repoRoot}`);
|
|
1129
|
+
}
|
|
1130
|
+
function toolFingerprint(toolName, scrubbed, repoRoot) {
|
|
1131
|
+
return hashValue(`tool:${toolName}:${canonicalStringify(scrubbed)}:${repoRoot}`);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// src/core/approval-replay-cli.ts
|
|
1135
|
+
import { spawn } from "node:child_process";
|
|
1136
|
+
import path4 from "node:path";
|
|
1137
|
+
var DEFAULT_CLI_REPLAY_TIMEOUT_MS = 3e5;
|
|
1138
|
+
async function replayShellCommand(command, cwd, timeoutMs = DEFAULT_CLI_REPLAY_TIMEOUT_MS) {
|
|
1139
|
+
const resolvedCwd = path4.resolve(cwd);
|
|
1140
|
+
return new Promise((resolve, reject) => {
|
|
1141
|
+
const child = spawn(command, {
|
|
1142
|
+
cwd: resolvedCwd,
|
|
1143
|
+
shell: true,
|
|
1144
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1145
|
+
env: { ...process.env }
|
|
1146
|
+
});
|
|
1147
|
+
const stdout = [];
|
|
1148
|
+
const stderr = [];
|
|
1149
|
+
let timedOut = false;
|
|
1150
|
+
const timer = setTimeout(() => {
|
|
1151
|
+
timedOut = true;
|
|
1152
|
+
child.kill("SIGTERM");
|
|
1153
|
+
}, timeoutMs);
|
|
1154
|
+
child.stdout?.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
|
|
1155
|
+
child.stderr?.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
|
|
1156
|
+
child.on("error", (error) => {
|
|
1157
|
+
clearTimeout(timer);
|
|
1158
|
+
reject(error);
|
|
1159
|
+
});
|
|
1160
|
+
child.on("close", (code) => {
|
|
1161
|
+
clearTimeout(timer);
|
|
1162
|
+
resolve({
|
|
1163
|
+
exitCode: timedOut ? 124 : code ?? 1,
|
|
1164
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
1165
|
+
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
1166
|
+
...timedOut ? { timedOut: true } : {}
|
|
1167
|
+
});
|
|
1168
|
+
});
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// src/core/approval-replay.ts
|
|
1173
|
+
var MAX_PAYLOAD_JSON_BYTES = 16384;
|
|
1174
|
+
function replayPayloadHash(kind, payload, repoRoot) {
|
|
1175
|
+
if (!payload || Object.keys(payload).length === 0) {
|
|
1176
|
+
return void 0;
|
|
1177
|
+
}
|
|
1178
|
+
const json = canonicalStringify(payload);
|
|
1179
|
+
return hashValue(`payload:${kind}:${json}:${repoRoot}`);
|
|
1180
|
+
}
|
|
1181
|
+
var DEFAULT_REPLAY_CAPABILITIES = {
|
|
1182
|
+
shell: true,
|
|
1183
|
+
tool: false,
|
|
1184
|
+
subagent: false
|
|
1185
|
+
};
|
|
1186
|
+
var ADAPTER_REPLAY_CAPABLE = {
|
|
1187
|
+
cursor: { ...DEFAULT_REPLAY_CAPABILITIES },
|
|
1188
|
+
claude: { ...DEFAULT_REPLAY_CAPABILITIES },
|
|
1189
|
+
codex: { ...DEFAULT_REPLAY_CAPABILITIES }
|
|
1190
|
+
};
|
|
1191
|
+
function getExecutionLeaseMs(config) {
|
|
1192
|
+
const ms = config.approval?.executionLeaseMs;
|
|
1193
|
+
return typeof ms === "number" && ms > 0 ? ms : 6e4;
|
|
1194
|
+
}
|
|
1195
|
+
function approvalFlow(config) {
|
|
1196
|
+
return config.approval?.flow === "two_step" ? "two_step" : "one_step";
|
|
1197
|
+
}
|
|
1198
|
+
function buildReplayEnvelopeFields(input) {
|
|
1199
|
+
const fields = {};
|
|
1200
|
+
const cmd = input.command ?? input.input ?? "";
|
|
1201
|
+
if (cmd) {
|
|
1202
|
+
fields.input = cmd;
|
|
1203
|
+
fields.inputKind = input.inputKind ?? input.kind;
|
|
1204
|
+
}
|
|
1205
|
+
if (input.cwd) {
|
|
1206
|
+
fields.cwd = path5.resolve(input.cwd);
|
|
1207
|
+
}
|
|
1208
|
+
if (input.toolName) {
|
|
1209
|
+
fields.toolName = input.toolName;
|
|
1210
|
+
}
|
|
1211
|
+
if (input.payload && Object.keys(input.payload).length > 0) {
|
|
1212
|
+
const json = canonicalStringify(input.payload);
|
|
1213
|
+
if (Buffer.byteLength(json, "utf8") <= MAX_PAYLOAD_JSON_BYTES) {
|
|
1214
|
+
fields.payloadJson = json;
|
|
1215
|
+
}
|
|
1216
|
+
fields.payloadHash = replayPayloadHash(input.kind, input.payload, input.repoRoot);
|
|
1217
|
+
}
|
|
1218
|
+
return fields;
|
|
1219
|
+
}
|
|
1220
|
+
function hasReplayEnvelope(approval) {
|
|
1221
|
+
return Boolean(approval.cwd || approval.toolName || approval.payloadHash);
|
|
1222
|
+
}
|
|
1223
|
+
function validateReplayEnvelope(approval, action) {
|
|
1224
|
+
if (!hasReplayEnvelope(approval)) {
|
|
1225
|
+
return true;
|
|
1226
|
+
}
|
|
1227
|
+
if (approval.kind !== action.kind) {
|
|
1228
|
+
return false;
|
|
1229
|
+
}
|
|
1230
|
+
if (approval.fingerprint !== action.fingerprint) {
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
if (approval.repoRoot !== action.repoRoot) {
|
|
1234
|
+
return false;
|
|
1235
|
+
}
|
|
1236
|
+
if (approval.cwd && path5.resolve(approval.cwd) !== path5.resolve(action.cwd ?? "")) {
|
|
1237
|
+
return false;
|
|
1238
|
+
}
|
|
1239
|
+
if (approval.toolName && approval.toolName !== action.toolName) {
|
|
1240
|
+
return false;
|
|
1241
|
+
}
|
|
1242
|
+
if (approval.payloadHash) {
|
|
1243
|
+
const hash = replayPayloadHash(action.kind, action.payload, action.repoRoot);
|
|
1244
|
+
if (!hash || approval.payloadHash !== hash) {
|
|
1245
|
+
return false;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
return true;
|
|
1249
|
+
}
|
|
1250
|
+
function canAutoReplay(config, kind, adapter) {
|
|
1251
|
+
if (config.approvalSigning?.required) {
|
|
1252
|
+
return false;
|
|
1253
|
+
}
|
|
1254
|
+
if (approvalFlow(config) !== "one_step") {
|
|
1255
|
+
return false;
|
|
1256
|
+
}
|
|
1257
|
+
if (kind !== "shell" && kind !== "tool" && kind !== "subagent") {
|
|
1258
|
+
return false;
|
|
1259
|
+
}
|
|
1260
|
+
const scopes = config.approval?.autoReplayScopes;
|
|
1261
|
+
if (kind === "shell" && scopes?.shell === false) {
|
|
1262
|
+
return false;
|
|
1263
|
+
}
|
|
1264
|
+
if (kind === "tool" && scopes?.tool !== true) {
|
|
1265
|
+
return false;
|
|
1266
|
+
}
|
|
1267
|
+
if (kind === "subagent" && scopes?.subagent !== true) {
|
|
1268
|
+
return false;
|
|
1269
|
+
}
|
|
1270
|
+
if (adapter) {
|
|
1271
|
+
return ADAPTER_REPLAY_CAPABLE[adapter][kind] === true;
|
|
1272
|
+
}
|
|
1273
|
+
return kind === "shell" && scopes?.shell !== false;
|
|
1274
|
+
}
|
|
1275
|
+
function buildRetryInstructionForConfig(config, tokenPrefix, approvalId) {
|
|
1276
|
+
if (approvalFlow(config) === "one_step") {
|
|
1277
|
+
return `To allow this action once, send ${tokenPrefix} ${approvalId}. After approval, retry the same action unchanged.`;
|
|
1278
|
+
}
|
|
1279
|
+
return `To allow the next matching action once, send ${tokenPrefix} ${approvalId} and then retry the original action unchanged.`;
|
|
1280
|
+
}
|
|
1281
|
+
function buildApprovalRecordedMessage(config, approval, adapter) {
|
|
1282
|
+
if (!canAutoReplay(config, approval.kind, adapter)) {
|
|
1283
|
+
return `Belay approval recorded for ${approval.approvalId}. Retry the original action once before it expires.`;
|
|
1284
|
+
}
|
|
1285
|
+
if (approval.kind === "shell" && approval.input) {
|
|
1286
|
+
return `Belay approval recorded for ${approval.approvalId}. Retry this shell command unchanged: ${approval.input}`;
|
|
1287
|
+
}
|
|
1288
|
+
return `Belay approval recorded for ${approval.approvalId}. Retry the original action once before it expires.`;
|
|
1289
|
+
}
|
|
1290
|
+
function buildReplayHint(config, approval, adapter) {
|
|
1291
|
+
if (approvalFlow(config) !== "one_step") {
|
|
1292
|
+
return null;
|
|
1293
|
+
}
|
|
1294
|
+
const input = approval.input ?? approval.summary;
|
|
1295
|
+
if (!input) {
|
|
1296
|
+
return null;
|
|
1297
|
+
}
|
|
1298
|
+
const autoReplay = canAutoReplay(config, approval.kind, adapter);
|
|
1299
|
+
return {
|
|
1300
|
+
kind: approval.kind,
|
|
1301
|
+
input,
|
|
1302
|
+
cwd: approval.cwd,
|
|
1303
|
+
toolName: approval.toolName,
|
|
1304
|
+
fingerprint: approval.fingerprint,
|
|
1305
|
+
approvalId: approval.approvalId,
|
|
1306
|
+
autoReplay,
|
|
1307
|
+
fallbackToTwoStep: !autoReplay
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
951
1310
|
|
|
952
1311
|
// src/core/approval.ts
|
|
953
|
-
var APPROVAL_EXECUTION_LEASE_MS =
|
|
1312
|
+
var APPROVAL_EXECUTION_LEASE_MS = DEFAULT_APPROVAL_CONFIG.executionLeaseMs;
|
|
954
1313
|
function nowIso() {
|
|
955
1314
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
956
1315
|
}
|
|
@@ -987,8 +1346,34 @@ function approvalCommandMatch(prompt, tokenPrefix) {
|
|
|
987
1346
|
}
|
|
988
1347
|
return null;
|
|
989
1348
|
}
|
|
990
|
-
function
|
|
991
|
-
|
|
1349
|
+
function createApprovalRecordWithEnvelope(params) {
|
|
1350
|
+
const envelope = buildReplayEnvelopeFields({
|
|
1351
|
+
kind: params.kind,
|
|
1352
|
+
command: params.approvalInput?.input,
|
|
1353
|
+
input: params.approvalInput?.input,
|
|
1354
|
+
inputKind: params.approvalInput?.inputKind,
|
|
1355
|
+
cwd: params.approvalInput?.cwd,
|
|
1356
|
+
toolName: params.approvalInput?.toolName,
|
|
1357
|
+
payload: params.approvalInput?.payload,
|
|
1358
|
+
fingerprint: params.fingerprint,
|
|
1359
|
+
repoRoot: params.repoRoot
|
|
1360
|
+
});
|
|
1361
|
+
return createApprovalRecord({
|
|
1362
|
+
kind: params.kind,
|
|
1363
|
+
fingerprint: params.fingerprint,
|
|
1364
|
+
repoRoot: params.repoRoot,
|
|
1365
|
+
reason: params.reason,
|
|
1366
|
+
summary: params.summary,
|
|
1367
|
+
approvalTtlMinutes: params.approvalTtlMinutes,
|
|
1368
|
+
approvalId: params.approvalId,
|
|
1369
|
+
input: envelope.input ?? params.approvalInput?.input,
|
|
1370
|
+
inputKind: envelope.inputKind ?? params.approvalInput?.inputKind,
|
|
1371
|
+
cwd: envelope.cwd,
|
|
1372
|
+
toolName: envelope.toolName ?? params.approvalInput?.toolName,
|
|
1373
|
+
payloadHash: envelope.payloadHash,
|
|
1374
|
+
payloadJson: envelope.payloadJson,
|
|
1375
|
+
scopeHint: params.scopeHint
|
|
1376
|
+
});
|
|
992
1377
|
}
|
|
993
1378
|
function createApprovalRecord(params) {
|
|
994
1379
|
const createdAt = nowIso();
|
|
@@ -1007,221 +1392,515 @@ function createApprovalRecord(params) {
|
|
|
1007
1392
|
record.input = params.input;
|
|
1008
1393
|
record.inputKind = params.inputKind ?? params.kind;
|
|
1009
1394
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
// src/core/approval-token.ts
|
|
1014
|
-
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
1015
|
-
import { existsSync } from "node:fs";
|
|
1016
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1017
|
-
import path4 from "node:path";
|
|
1018
|
-
function base64UrlEncode(value) {
|
|
1019
|
-
return Buffer.from(value, "utf8").toString("base64url");
|
|
1020
|
-
}
|
|
1021
|
-
function base64UrlDecode(value) {
|
|
1022
|
-
return Buffer.from(value, "base64url").toString("utf8");
|
|
1023
|
-
}
|
|
1024
|
-
function approvalSigningKeyPath(controlPlaneDir = defaultControlPlaneDir()) {
|
|
1025
|
-
return path4.join(controlPlaneDir, "approval-signing.key");
|
|
1026
|
-
}
|
|
1027
|
-
async function loadOrCreateApprovalSigningKey(controlPlaneDir = defaultControlPlaneDir()) {
|
|
1028
|
-
const keyPath = approvalSigningKeyPath(controlPlaneDir);
|
|
1029
|
-
if (existsSync(keyPath)) {
|
|
1030
|
-
return readFile(keyPath);
|
|
1395
|
+
if (params.cwd) {
|
|
1396
|
+
record.cwd = params.cwd;
|
|
1031
1397
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
await writeFile(keyPath, key, { mode: 384 });
|
|
1035
|
-
return key;
|
|
1036
|
-
}
|
|
1037
|
-
function signPayload(payload, key) {
|
|
1038
|
-
const body = base64UrlEncode(JSON.stringify(payload));
|
|
1039
|
-
const signature = createHmac("sha256", key).update(body).digest("base64url");
|
|
1040
|
-
return `${body}.${signature}`;
|
|
1041
|
-
}
|
|
1042
|
-
async function issueApprovalToken(payload, controlPlaneDir = defaultControlPlaneDir()) {
|
|
1043
|
-
const key = await loadOrCreateApprovalSigningKey(controlPlaneDir);
|
|
1044
|
-
return signPayload(payload, key);
|
|
1045
|
-
}
|
|
1046
|
-
async function verifyApprovalToken(token, controlPlaneDir = defaultControlPlaneDir()) {
|
|
1047
|
-
const [body, signature] = token.split(".");
|
|
1048
|
-
if (!body || !signature) {
|
|
1049
|
-
return null;
|
|
1398
|
+
if (params.toolName) {
|
|
1399
|
+
record.toolName = params.toolName;
|
|
1050
1400
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
return null;
|
|
1401
|
+
if (params.payloadHash) {
|
|
1402
|
+
record.payloadHash = params.payloadHash;
|
|
1054
1403
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
const actualBuffer = Buffer.from(signature);
|
|
1058
|
-
const expectedBuffer = Buffer.from(expected);
|
|
1059
|
-
if (actualBuffer.length !== expectedBuffer.length || !timingSafeEqual(actualBuffer, expectedBuffer)) {
|
|
1060
|
-
return null;
|
|
1404
|
+
if (params.payloadJson) {
|
|
1405
|
+
record.payloadJson = params.payloadJson;
|
|
1061
1406
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
if (Date.parse(payload.expiresAt) <= Date.now()) {
|
|
1065
|
-
return null;
|
|
1066
|
-
}
|
|
1067
|
-
return payload;
|
|
1068
|
-
} catch {
|
|
1069
|
-
return null;
|
|
1407
|
+
if (params.scopeHint) {
|
|
1408
|
+
record.scopeHint = params.scopeHint;
|
|
1070
1409
|
}
|
|
1410
|
+
return record;
|
|
1071
1411
|
}
|
|
1072
1412
|
|
|
1073
|
-
// src/
|
|
1074
|
-
async function recordApproval(params) {
|
|
1075
|
-
const { approvalId, config, store, token, requireSignedToken = false } = params;
|
|
1076
|
-
const pending = await store.loadPending();
|
|
1077
|
-
pending.state = compactApprovals(pending.state);
|
|
1078
|
-
const index = pending.state.approvals.findIndex((approval2) => approval2.approvalId === approvalId);
|
|
1079
|
-
if (index === -1) {
|
|
1080
|
-
await store.writePending(pending.filePath, pending.state);
|
|
1081
|
-
return { ok: false, message: "Belay approval not found or expired." };
|
|
1082
|
-
}
|
|
1083
|
-
const [approval] = pending.state.approvals.slice(index, index + 1);
|
|
1084
|
-
if (requireSignedToken) {
|
|
1085
|
-
if (!token) {
|
|
1086
|
-
return { ok: false, message: "Signed approval token required for out-of-band approval." };
|
|
1087
|
-
}
|
|
1088
|
-
const controlPlaneDir = configuredControlPlaneDir(config);
|
|
1089
|
-
const verified = await verifyApprovalToken(token, controlPlaneDir);
|
|
1090
|
-
if (!verified || verified.approvalId !== approvalId) {
|
|
1091
|
-
return { ok: false, message: "Invalid or expired signed approval token." };
|
|
1092
|
-
}
|
|
1093
|
-
if (verified.fingerprint !== approval.fingerprint || verified.repoRoot !== approval.repoRoot) {
|
|
1094
|
-
return { ok: false, message: "Signed approval token does not match the pending approval." };
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
pending.state.approvals.splice(index, 1);
|
|
1098
|
-
await store.writePending(pending.filePath, pending.state);
|
|
1099
|
-
const approved = await store.loadApproved();
|
|
1100
|
-
approved.state = compactApprovals(approved.state);
|
|
1101
|
-
approved.state.approvals.push({
|
|
1102
|
-
...approval,
|
|
1103
|
-
approvedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1104
|
-
});
|
|
1105
|
-
await store.writeApproved(approved.filePath, approved.state);
|
|
1106
|
-
return {
|
|
1107
|
-
ok: true,
|
|
1108
|
-
message: `Belay approval recorded for ${approvalId}. Retry the original action once before it expires.`,
|
|
1109
|
-
approval
|
|
1110
|
-
};
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
// src/core/capability/allowlist.ts
|
|
1114
|
-
import { existsSync as existsSync3, readFileSync } from "node:fs";
|
|
1413
|
+
// src/adapters/layouts/codex.ts
|
|
1115
1414
|
import path6 from "node:path";
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
import path5 from "node:path";
|
|
1120
|
-
function canonicalPath(targetPath) {
|
|
1121
|
-
const resolved = path5.resolve(targetPath);
|
|
1122
|
-
if (!resolved) {
|
|
1123
|
-
return resolved;
|
|
1124
|
-
}
|
|
1125
|
-
const parsed = path5.parse(resolved);
|
|
1126
|
-
let current = parsed.root;
|
|
1127
|
-
const relativeParts = path5.relative(parsed.root || ".", resolved).split(path5.sep).filter(Boolean);
|
|
1128
|
-
for (let i = 0; i < relativeParts.length; i++) {
|
|
1129
|
-
const segment = relativeParts[i];
|
|
1130
|
-
if (!segment) {
|
|
1131
|
-
continue;
|
|
1132
|
-
}
|
|
1133
|
-
const candidate = current === "" ? segment : path5.join(current, segment);
|
|
1134
|
-
if (!existsSync2(candidate)) {
|
|
1135
|
-
return path5.join(candidate, ...relativeParts.slice(i + 1));
|
|
1136
|
-
}
|
|
1137
|
-
try {
|
|
1138
|
-
current = realpathSync.native(candidate);
|
|
1139
|
-
} catch {
|
|
1140
|
-
return path5.join(candidate, ...relativeParts.slice(i + 1));
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
return current;
|
|
1415
|
+
function runnerCommand2(platform, repoRoot, hookName, ...args) {
|
|
1416
|
+
const hooksDir = path6.join(path6.resolve(repoRoot), ".codex", "hooks");
|
|
1417
|
+
return buildRunnerInvocation(platform, hooksDir, repoRoot, hookName, ...args);
|
|
1144
1418
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1419
|
+
var codexLayout = {
|
|
1420
|
+
name: "codex",
|
|
1421
|
+
configPath(repoRoot) {
|
|
1422
|
+
return path6.join(repoRoot, ".codex", "belay.config.json");
|
|
1423
|
+
},
|
|
1424
|
+
// Codex reads lifecycle hooks from `.codex/config.toml` (project layer).
|
|
1425
|
+
hooksSettingsPath(repoRoot) {
|
|
1426
|
+
return path6.join(repoRoot, ".codex", "config.toml");
|
|
1427
|
+
},
|
|
1428
|
+
hooksDir(repoRoot) {
|
|
1429
|
+
return path6.join(repoRoot, ".codex", "hooks");
|
|
1430
|
+
},
|
|
1431
|
+
runtimeDir(repoRoot) {
|
|
1432
|
+
return path6.join(repoRoot, ".codex", "belay", "runtime");
|
|
1433
|
+
},
|
|
1434
|
+
repoLocalStateDir(repoRoot) {
|
|
1435
|
+
return path6.join(repoRoot, ".codex", "belay");
|
|
1436
|
+
},
|
|
1437
|
+
defaultAuditLogPath(_repoRoot) {
|
|
1438
|
+
return path6.join(".codex", "belay", "audit.ndjson");
|
|
1439
|
+
},
|
|
1440
|
+
repoRootMarkers: [".git", ".codex"],
|
|
1441
|
+
runnerCommand: runnerCommand2,
|
|
1442
|
+
defaultConfig(repoRoot) {
|
|
1443
|
+
return {
|
|
1444
|
+
...DEFAULT_CONFIG_V4,
|
|
1445
|
+
adapter: "codex",
|
|
1446
|
+
audit: {
|
|
1447
|
+
...DEFAULT_CONFIG_V4.audit,
|
|
1448
|
+
logPath: codexLayout.defaultAuditLogPath(repoRoot)
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1151
1451
|
}
|
|
1152
|
-
|
|
1452
|
+
};
|
|
1453
|
+
|
|
1454
|
+
// src/adapters/layouts/cursor.ts
|
|
1455
|
+
import path7 from "node:path";
|
|
1456
|
+
function runnerCommand3(platform, repoRoot, hookName, ...args) {
|
|
1457
|
+
const hooksDir = path7.join(path7.resolve(repoRoot), ".cursor", "hooks");
|
|
1458
|
+
return buildRunnerInvocation(platform, hooksDir, repoRoot, hookName, ...args);
|
|
1153
1459
|
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1460
|
+
var cursorLayout = {
|
|
1461
|
+
name: "cursor",
|
|
1462
|
+
configPath(repoRoot) {
|
|
1463
|
+
return path7.join(repoRoot, ".cursor", "belay.config.json");
|
|
1464
|
+
},
|
|
1465
|
+
hooksSettingsPath(repoRoot) {
|
|
1466
|
+
return path7.join(repoRoot, ".cursor", "hooks.json");
|
|
1467
|
+
},
|
|
1468
|
+
hooksDir(repoRoot) {
|
|
1469
|
+
return path7.join(repoRoot, ".cursor", "hooks");
|
|
1470
|
+
},
|
|
1471
|
+
runtimeDir(repoRoot) {
|
|
1472
|
+
return path7.join(repoRoot, ".cursor", "belay", "runtime");
|
|
1473
|
+
},
|
|
1474
|
+
repoLocalStateDir(repoRoot) {
|
|
1475
|
+
return path7.join(repoRoot, ".cursor", "belay");
|
|
1476
|
+
},
|
|
1477
|
+
defaultAuditLogPath(_repoRoot) {
|
|
1478
|
+
return path7.join(".cursor", "belay", "audit.ndjson");
|
|
1479
|
+
},
|
|
1480
|
+
repoRootMarkers: [".git", ".cursor"],
|
|
1481
|
+
runnerCommand: runnerCommand3,
|
|
1482
|
+
defaultConfig(repoRoot) {
|
|
1483
|
+
return {
|
|
1484
|
+
...DEFAULT_CONFIG_V4,
|
|
1485
|
+
adapter: "cursor",
|
|
1486
|
+
audit: {
|
|
1487
|
+
...DEFAULT_CONFIG_V4.audit,
|
|
1488
|
+
logPath: cursorLayout.defaultAuditLogPath(repoRoot)
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1163
1491
|
}
|
|
1164
|
-
|
|
1492
|
+
};
|
|
1493
|
+
|
|
1494
|
+
// src/adapters/layouts/index.ts
|
|
1495
|
+
var layouts = {
|
|
1496
|
+
cursor: cursorLayout,
|
|
1497
|
+
claude: claudeLayout,
|
|
1498
|
+
codex: codexLayout
|
|
1499
|
+
};
|
|
1500
|
+
function getAdapterLayout(name = "cursor") {
|
|
1501
|
+
return layouts[name];
|
|
1165
1502
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1503
|
+
|
|
1504
|
+
// src/core/config-layers.ts
|
|
1505
|
+
import path8 from "node:path";
|
|
1506
|
+
|
|
1507
|
+
// src/presets.ts
|
|
1508
|
+
var CONFIG_PRESETS = {
|
|
1509
|
+
strict: {
|
|
1510
|
+
mode: "enforce",
|
|
1511
|
+
policy: {
|
|
1512
|
+
...DEFAULT_CONFIG_V3.policy,
|
|
1513
|
+
unknownLocalEffect: "deny",
|
|
1514
|
+
unparseableShell: "deny",
|
|
1515
|
+
confidenceThresholds: { allow: 0.9, flag: 0.8 },
|
|
1516
|
+
modelAssist: { enabled: false }
|
|
1517
|
+
},
|
|
1518
|
+
sandbox: { ...DEFAULT_CONFIG_V3.sandbox }
|
|
1519
|
+
},
|
|
1520
|
+
standard: {
|
|
1521
|
+
mode: "enforce"
|
|
1522
|
+
},
|
|
1523
|
+
"audit-first": {
|
|
1524
|
+
mode: "audit",
|
|
1525
|
+
policy: {
|
|
1526
|
+
...DEFAULT_CONFIG_V3.policy,
|
|
1527
|
+
unknownLocalEffect: "deny",
|
|
1528
|
+
unparseableShell: "deny",
|
|
1529
|
+
confidenceThresholds: { allow: 0.88, flag: 0.72 },
|
|
1530
|
+
modelAssist: { enabled: false }
|
|
1531
|
+
},
|
|
1532
|
+
sandbox: { ...DEFAULT_CONFIG_V3.sandbox }
|
|
1533
|
+
},
|
|
1534
|
+
"l1-full-recommended": {
|
|
1535
|
+
mode: "enforce",
|
|
1536
|
+
policy: {
|
|
1537
|
+
...DEFAULT_CONFIG_V3.policy,
|
|
1538
|
+
confidenceThresholds: { ...DEFAULT_CONFIG_V3.policy.confidenceThresholds },
|
|
1539
|
+
modelAssist: { ...DEFAULT_CONFIG_V3.policy.modelAssist }
|
|
1540
|
+
},
|
|
1541
|
+
sandbox: {
|
|
1542
|
+
enabled: true,
|
|
1543
|
+
runtime: "container",
|
|
1544
|
+
denyNetworkByDefault: true
|
|
1545
|
+
},
|
|
1546
|
+
egress: {
|
|
1547
|
+
...DEFAULT_CONFIG_V3.egress,
|
|
1548
|
+
enabled: true,
|
|
1549
|
+
demoteL3External: true
|
|
1550
|
+
},
|
|
1551
|
+
approvalSigning: {
|
|
1552
|
+
required: true
|
|
1553
|
+
},
|
|
1554
|
+
controlPlane: {
|
|
1555
|
+
...DEFAULT_CONFIG_V3.controlPlane,
|
|
1556
|
+
isolation: {
|
|
1557
|
+
mode: "separate-user",
|
|
1558
|
+
verifyAgentWritable: true
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1181
1561
|
}
|
|
1182
|
-
|
|
1562
|
+
};
|
|
1563
|
+
function applyConfigPreset(preset, extra = {}) {
|
|
1564
|
+
const base = CONFIG_PRESETS[preset] ?? CONFIG_PRESETS.standard;
|
|
1565
|
+
return {
|
|
1566
|
+
version: 3,
|
|
1567
|
+
...base,
|
|
1568
|
+
...extra,
|
|
1569
|
+
policy: {
|
|
1570
|
+
...DEFAULT_CONFIG_V3.policy,
|
|
1571
|
+
...base.policy ?? {},
|
|
1572
|
+
...extra.policy
|
|
1573
|
+
},
|
|
1574
|
+
sandbox: {
|
|
1575
|
+
...DEFAULT_CONFIG_V3.sandbox,
|
|
1576
|
+
...base.sandbox ?? {},
|
|
1577
|
+
...extra.sandbox
|
|
1578
|
+
},
|
|
1579
|
+
egress: {
|
|
1580
|
+
...DEFAULT_CONFIG_V3.egress,
|
|
1581
|
+
...base.egress ?? {},
|
|
1582
|
+
...extra.egress
|
|
1583
|
+
},
|
|
1584
|
+
approvalSigning: {
|
|
1585
|
+
...DEFAULT_CONFIG_V3.approvalSigning,
|
|
1586
|
+
...base.approvalSigning ?? {},
|
|
1587
|
+
...extra.approvalSigning
|
|
1588
|
+
},
|
|
1589
|
+
controlPlane: {
|
|
1590
|
+
...DEFAULT_CONFIG_V3.controlPlane,
|
|
1591
|
+
...base.controlPlane ?? {},
|
|
1592
|
+
...extra.controlPlane,
|
|
1593
|
+
isolation: {
|
|
1594
|
+
...DEFAULT_CONFIG_V3.controlPlane.isolation,
|
|
1595
|
+
...base.controlPlane?.isolation ?? {},
|
|
1596
|
+
...extra.controlPlane?.isolation ?? {}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
};
|
|
1183
1600
|
}
|
|
1184
1601
|
|
|
1185
|
-
// src/core/
|
|
1186
|
-
function
|
|
1187
|
-
|
|
1602
|
+
// src/core/config-layers.ts
|
|
1603
|
+
function teamConfigPath(homedir = () => process.env.HOME ?? process.env.USERPROFILE ?? "") {
|
|
1604
|
+
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
1605
|
+
const base = xdg || path8.join(homedir(), ".config");
|
|
1606
|
+
return path8.join(base, "agent-belay", "team.config.json");
|
|
1188
1607
|
}
|
|
1189
|
-
function
|
|
1190
|
-
|
|
1191
|
-
|
|
1608
|
+
function applyProtectedLayer(config, builtin) {
|
|
1609
|
+
const controlPlane = { ...config.controlPlane };
|
|
1610
|
+
if (builtin.controlPlane.enabled && controlPlane.enabled === false) {
|
|
1611
|
+
controlPlane.enabled = true;
|
|
1612
|
+
}
|
|
1613
|
+
if (builtin.controlPlane.integrity === "hash-pinned" && controlPlane.integrity === "none") {
|
|
1614
|
+
controlPlane.integrity = "hash-pinned";
|
|
1192
1615
|
}
|
|
1193
|
-
const raw = JSON.parse(readFileSync(filePath, "utf8"));
|
|
1194
1616
|
return {
|
|
1195
|
-
|
|
1196
|
-
|
|
1617
|
+
...config,
|
|
1618
|
+
controlPlane
|
|
1197
1619
|
};
|
|
1198
1620
|
}
|
|
1199
|
-
function
|
|
1200
|
-
|
|
1621
|
+
function asV3Layer(raw) {
|
|
1622
|
+
if (!raw || typeof raw !== "object") {
|
|
1623
|
+
return { version: 3 };
|
|
1624
|
+
}
|
|
1625
|
+
return { version: 3, ...raw };
|
|
1201
1626
|
}
|
|
1202
|
-
function
|
|
1203
|
-
const
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1627
|
+
function mergeConfigLayer(base, layer) {
|
|
1628
|
+
const merged = mergeConfig(layer, base);
|
|
1629
|
+
if (!layer.policy) {
|
|
1630
|
+
return { ...merged, policy: base.policy };
|
|
1631
|
+
}
|
|
1632
|
+
return merged;
|
|
1208
1633
|
}
|
|
1209
|
-
function
|
|
1210
|
-
|
|
1634
|
+
function resolveLayeredConfig(params) {
|
|
1635
|
+
const provenance = [{ path: "(builtin)", source: "builtin" }];
|
|
1636
|
+
let config = mergeConfig({}, params.adapterDefaults);
|
|
1637
|
+
if (params.teamConfig) {
|
|
1638
|
+
const teamFile = params.teamConfig;
|
|
1639
|
+
const teamRaw = teamFile.preset ? applyConfigPreset(teamFile.preset, teamFile.config ?? {}) : teamFile.config ?? params.teamConfig;
|
|
1640
|
+
rejectTeamLayerJudgeSecrets(
|
|
1641
|
+
teamRaw.judge,
|
|
1642
|
+
"team"
|
|
1643
|
+
);
|
|
1644
|
+
config = mergeConfigLayer(config, asV3Layer(teamRaw));
|
|
1645
|
+
provenance.push({
|
|
1646
|
+
path: params.teamConfigPath ?? teamConfigPath(),
|
|
1647
|
+
source: "team"
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
config = mergeConfigLayer(config, asV3Layer(params.repoConfig));
|
|
1651
|
+
if (params.repoConfigPath) {
|
|
1652
|
+
provenance.push({ path: params.repoConfigPath, source: "repo" });
|
|
1653
|
+
}
|
|
1654
|
+
const protectedConfig = applyProtectedLayer(config, DEFAULT_CONFIG_V3);
|
|
1655
|
+
if (JSON.stringify(protectedConfig) !== JSON.stringify(config)) {
|
|
1656
|
+
provenance.push({ path: "(protected-layer)", source: "protected" });
|
|
1657
|
+
config = protectedConfig;
|
|
1658
|
+
}
|
|
1659
|
+
return { config, provenance };
|
|
1211
1660
|
}
|
|
1212
1661
|
|
|
1213
|
-
// src/
|
|
1214
|
-
function
|
|
1215
|
-
|
|
1662
|
+
// src/config-io.ts
|
|
1663
|
+
function resolveAdapterName(config) {
|
|
1664
|
+
if (config.adapter === "claude") {
|
|
1665
|
+
return "claude";
|
|
1666
|
+
}
|
|
1667
|
+
if (config.adapter === "codex") {
|
|
1668
|
+
return "codex";
|
|
1669
|
+
}
|
|
1670
|
+
return "cursor";
|
|
1216
1671
|
}
|
|
1217
|
-
function
|
|
1218
|
-
return
|
|
1672
|
+
function repoLocalStateDirFor(repoRoot, config) {
|
|
1673
|
+
return getAdapterLayout(resolveAdapterName(config)).repoLocalStateDir(repoRoot);
|
|
1219
1674
|
}
|
|
1220
1675
|
|
|
1221
|
-
// src/core/
|
|
1222
|
-
import
|
|
1676
|
+
// src/core/approval-token.ts
|
|
1677
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
1678
|
+
import { existsSync } from "node:fs";
|
|
1679
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1680
|
+
import path9 from "node:path";
|
|
1681
|
+
function base64UrlEncode(value) {
|
|
1682
|
+
return Buffer.from(value, "utf8").toString("base64url");
|
|
1683
|
+
}
|
|
1684
|
+
function base64UrlDecode(value) {
|
|
1685
|
+
return Buffer.from(value, "base64url").toString("utf8");
|
|
1686
|
+
}
|
|
1687
|
+
function approvalSigningKeyPath(controlPlaneDir = defaultControlPlaneDir()) {
|
|
1688
|
+
return path9.join(controlPlaneDir, "approval-signing.key");
|
|
1689
|
+
}
|
|
1690
|
+
async function loadOrCreateApprovalSigningKey(controlPlaneDir = defaultControlPlaneDir()) {
|
|
1691
|
+
const keyPath = approvalSigningKeyPath(controlPlaneDir);
|
|
1692
|
+
if (existsSync(keyPath)) {
|
|
1693
|
+
return readFile(keyPath);
|
|
1694
|
+
}
|
|
1695
|
+
await mkdir(controlPlaneDir, { recursive: true });
|
|
1696
|
+
const key = randomBytes(32);
|
|
1697
|
+
await writeFile(keyPath, key, { mode: 384 });
|
|
1698
|
+
return key;
|
|
1699
|
+
}
|
|
1700
|
+
function signPayload(payload, key) {
|
|
1701
|
+
const body = base64UrlEncode(JSON.stringify(payload));
|
|
1702
|
+
const signature = createHmac("sha256", key).update(body).digest("base64url");
|
|
1703
|
+
return `${body}.${signature}`;
|
|
1704
|
+
}
|
|
1705
|
+
async function issueApprovalToken(payload, controlPlaneDir = defaultControlPlaneDir()) {
|
|
1706
|
+
const key = await loadOrCreateApprovalSigningKey(controlPlaneDir);
|
|
1707
|
+
return signPayload(payload, key);
|
|
1708
|
+
}
|
|
1709
|
+
async function verifyApprovalToken(token, controlPlaneDir = defaultControlPlaneDir()) {
|
|
1710
|
+
const [body, signature] = token.split(".");
|
|
1711
|
+
if (!body || !signature) {
|
|
1712
|
+
return null;
|
|
1713
|
+
}
|
|
1714
|
+
const keyPath = approvalSigningKeyPath(controlPlaneDir);
|
|
1715
|
+
if (!existsSync(keyPath)) {
|
|
1716
|
+
return null;
|
|
1717
|
+
}
|
|
1718
|
+
const key = await readFile(keyPath);
|
|
1719
|
+
const expected = createHmac("sha256", key).update(body).digest("base64url");
|
|
1720
|
+
const actualBuffer = Buffer.from(signature);
|
|
1721
|
+
const expectedBuffer = Buffer.from(expected);
|
|
1722
|
+
if (actualBuffer.length !== expectedBuffer.length || !timingSafeEqual(actualBuffer, expectedBuffer)) {
|
|
1723
|
+
return null;
|
|
1724
|
+
}
|
|
1725
|
+
try {
|
|
1726
|
+
const payload = JSON.parse(base64UrlDecode(body));
|
|
1727
|
+
if (Date.parse(payload.expiresAt) <= Date.now()) {
|
|
1728
|
+
return null;
|
|
1729
|
+
}
|
|
1730
|
+
return payload;
|
|
1731
|
+
} catch {
|
|
1732
|
+
return null;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// src/core/approval-service.ts
|
|
1737
|
+
async function recordApproval(params) {
|
|
1738
|
+
const { approvalId, config, store, token, requireSignedToken = false, adapter } = params;
|
|
1739
|
+
const pending = await store.loadPending();
|
|
1740
|
+
pending.state = compactApprovals(pending.state);
|
|
1741
|
+
const index = pending.state.approvals.findIndex((approval2) => approval2.approvalId === approvalId);
|
|
1742
|
+
if (index === -1) {
|
|
1743
|
+
await store.writePending(pending.filePath, pending.state);
|
|
1744
|
+
return { ok: false, message: "Belay approval not found or expired." };
|
|
1745
|
+
}
|
|
1746
|
+
const [approval] = pending.state.approvals.slice(index, index + 1);
|
|
1747
|
+
if (requireSignedToken) {
|
|
1748
|
+
if (!token) {
|
|
1749
|
+
return { ok: false, message: "Signed approval token required for out-of-band approval." };
|
|
1750
|
+
}
|
|
1751
|
+
const controlPlaneDir = configuredControlPlaneDir(config);
|
|
1752
|
+
const verified = await verifyApprovalToken(token, controlPlaneDir);
|
|
1753
|
+
if (!verified || verified.approvalId !== approvalId) {
|
|
1754
|
+
return { ok: false, message: "Invalid or expired signed approval token." };
|
|
1755
|
+
}
|
|
1756
|
+
if (verified.fingerprint !== approval.fingerprint || verified.repoRoot !== approval.repoRoot) {
|
|
1757
|
+
return { ok: false, message: "Signed approval token does not match the pending approval." };
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
pending.state.approvals.splice(index, 1);
|
|
1761
|
+
await store.writePending(pending.filePath, pending.state);
|
|
1762
|
+
const approved = await store.loadApproved();
|
|
1763
|
+
approved.state = compactApprovals(approved.state);
|
|
1764
|
+
approved.state.approvals.push({
|
|
1765
|
+
...approval,
|
|
1766
|
+
approvedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1767
|
+
});
|
|
1768
|
+
await store.writeApproved(approved.filePath, approved.state);
|
|
1769
|
+
return {
|
|
1770
|
+
ok: true,
|
|
1771
|
+
message: buildApprovalRecordedMessage(config, approval, adapter),
|
|
1772
|
+
approval
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
async function consumeApprovedAfterCliReplay(params) {
|
|
1776
|
+
const approved = await params.store.loadApproved();
|
|
1777
|
+
approved.state = compactApprovals(approved.state);
|
|
1778
|
+
const remaining = approved.state.approvals.filter(
|
|
1779
|
+
(approval) => approval.approvalId !== params.approvalId
|
|
1780
|
+
);
|
|
1781
|
+
if (remaining.length === approved.state.approvals.length) {
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
approved.state.approvals = remaining;
|
|
1785
|
+
await params.store.writeApproved(approved.filePath, approved.state);
|
|
1786
|
+
}
|
|
1787
|
+
function gateApprovalStoreFromDeps(deps) {
|
|
1788
|
+
return {
|
|
1789
|
+
loadPending: () => deps.loadApprovals("pending-approvals.json"),
|
|
1790
|
+
loadApproved: () => deps.loadApprovals("approved-approvals.json"),
|
|
1791
|
+
writePending: (filePath, state) => deps.writeApprovals(filePath, state),
|
|
1792
|
+
writeApproved: (filePath, state) => deps.writeApprovals(filePath, state)
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
// src/core/audit-replay-context.ts
|
|
1797
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1798
|
+
function hashReplayPayload(payload) {
|
|
1799
|
+
return createHash2("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
1800
|
+
}
|
|
1801
|
+
function buildAuditActionSnapshot(kind, result, replayAction) {
|
|
1802
|
+
if (!replayAction?.cwd) {
|
|
1803
|
+
return void 0;
|
|
1804
|
+
}
|
|
1805
|
+
const replayKind = replayAction.kind;
|
|
1806
|
+
const resolvedKind = replayKind === "shell" || replayKind === "tool" || replayKind === "subagent" ? replayKind : kind;
|
|
1807
|
+
const normalizedAction = result.normalizedCommand ?? replayAction.command ?? result.summary ?? "";
|
|
1808
|
+
if (!normalizedAction.trim()) {
|
|
1809
|
+
return void 0;
|
|
1810
|
+
}
|
|
1811
|
+
return {
|
|
1812
|
+
schemaVersion: 1,
|
|
1813
|
+
kind: resolvedKind,
|
|
1814
|
+
cwd: replayAction.cwd,
|
|
1815
|
+
normalizedAction,
|
|
1816
|
+
...replayAction.toolName ? { toolName: replayAction.toolName } : {},
|
|
1817
|
+
...replayAction.payload ? { payloadHash: hashReplayPayload(replayAction.payload) } : {}
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
function buildAuditReplayContext(kind, result, replayAction) {
|
|
1821
|
+
if (!replayAction?.cwd) {
|
|
1822
|
+
return void 0;
|
|
1823
|
+
}
|
|
1824
|
+
const replayKind = replayAction.kind;
|
|
1825
|
+
const resolvedKind = replayKind === "shell" || replayKind === "tool" || replayKind === "subagent" ? replayKind : kind;
|
|
1826
|
+
return {
|
|
1827
|
+
cwd: replayAction.cwd,
|
|
1828
|
+
kind: resolvedKind,
|
|
1829
|
+
command: replayAction.command ?? result.normalizedCommand ?? result.summary,
|
|
1830
|
+
...replayAction.toolName ? { toolName: replayAction.toolName } : {},
|
|
1831
|
+
...replayAction.payload ? { payload: replayAction.payload } : {}
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
// src/core/capability/allowlist.ts
|
|
1836
|
+
import { existsSync as existsSync3, readFileSync } from "node:fs";
|
|
1837
|
+
import path11 from "node:path";
|
|
1838
|
+
|
|
1839
|
+
// src/core/path-utils.ts
|
|
1840
|
+
import { existsSync as existsSync2, realpathSync, statSync } from "node:fs";
|
|
1841
|
+
import path10 from "node:path";
|
|
1223
1842
|
|
|
1224
1843
|
// src/core/shell-tokenizer.ts
|
|
1844
|
+
var FD_DUPLICATION_PATTERN = /^\d+[<>]&(?:\d+|-)$/;
|
|
1845
|
+
var FD_REDIRECT_PATTERN = /^\d+(?:>>?|<)$/;
|
|
1846
|
+
function readDigits(input, index) {
|
|
1847
|
+
let end = index;
|
|
1848
|
+
while (end < input.length && /[0-9]/.test(input[end] ?? "")) {
|
|
1849
|
+
end += 1;
|
|
1850
|
+
}
|
|
1851
|
+
return input.slice(index, end);
|
|
1852
|
+
}
|
|
1853
|
+
function readShellOperator(input, index) {
|
|
1854
|
+
const char = input[index];
|
|
1855
|
+
const next = input[index + 1] ?? "";
|
|
1856
|
+
const digits = /[0-9]/.test(char) ? readDigits(input, index) : "";
|
|
1857
|
+
const digitsLength = digits.length;
|
|
1858
|
+
const afterDigits = digitsLength > 0 ? input[index + digitsLength] ?? "" : "";
|
|
1859
|
+
const afterDigitsNext = digitsLength > 0 ? input[index + digitsLength + 1] ?? "" : "";
|
|
1860
|
+
if (char === "&" && next === "&") {
|
|
1861
|
+
return { token: "&&", length: 2 };
|
|
1862
|
+
}
|
|
1863
|
+
if (char === "|" && next === "|") {
|
|
1864
|
+
return { token: "||", length: 2 };
|
|
1865
|
+
}
|
|
1866
|
+
if (char === "|" && next === "&") {
|
|
1867
|
+
return { token: "|&", length: 2 };
|
|
1868
|
+
}
|
|
1869
|
+
if (char === "&" && next === ">") {
|
|
1870
|
+
return { token: "&>", length: 2 };
|
|
1871
|
+
}
|
|
1872
|
+
if (digitsLength > 0 && (afterDigits === ">" || afterDigits === "<")) {
|
|
1873
|
+
if (afterDigitsNext === "&") {
|
|
1874
|
+
let end = index + digitsLength + 2;
|
|
1875
|
+
while (end < input.length && /[0-9-]/.test(input[end] ?? "")) {
|
|
1876
|
+
end += 1;
|
|
1877
|
+
}
|
|
1878
|
+
if (end > index + digitsLength + 2) {
|
|
1879
|
+
return { token: input.slice(index, end), length: end - index };
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
if (afterDigits === ">" && afterDigitsNext === ">") {
|
|
1883
|
+
return { token: `${digits}>>`, length: digitsLength + 2 };
|
|
1884
|
+
}
|
|
1885
|
+
return { token: `${digits}${afterDigits}`, length: digitsLength + 1 };
|
|
1886
|
+
}
|
|
1887
|
+
if (char === ">" && next === ">") {
|
|
1888
|
+
return { token: ">>", length: 2 };
|
|
1889
|
+
}
|
|
1890
|
+
if (char === "&") {
|
|
1891
|
+
return { token: "&", length: 1 };
|
|
1892
|
+
}
|
|
1893
|
+
if (char === "|" || char === ";" || char === ">" || char === "<") {
|
|
1894
|
+
return { token: char, length: 1 };
|
|
1895
|
+
}
|
|
1896
|
+
return null;
|
|
1897
|
+
}
|
|
1898
|
+
function isRedirectOperator(token) {
|
|
1899
|
+
return token === ">" || token === ">>" || token === "<" || token === "&>" || FD_REDIRECT_PATTERN.test(token);
|
|
1900
|
+
}
|
|
1901
|
+
function isFdDuplication(token) {
|
|
1902
|
+
return FD_DUPLICATION_PATTERN.test(token);
|
|
1903
|
+
}
|
|
1225
1904
|
function tokenizeShell(input) {
|
|
1226
1905
|
const tokens = [];
|
|
1227
1906
|
let buffer = "";
|
|
@@ -1235,7 +1914,6 @@ function tokenizeShell(input) {
|
|
|
1235
1914
|
};
|
|
1236
1915
|
for (let index = 0; index < input.length; index += 1) {
|
|
1237
1916
|
const char = input[index];
|
|
1238
|
-
const next = input[index + 1] ?? "";
|
|
1239
1917
|
if (escaping) {
|
|
1240
1918
|
buffer += char;
|
|
1241
1919
|
escaping = false;
|
|
@@ -1257,27 +1935,11 @@ function tokenizeShell(input) {
|
|
|
1257
1935
|
quote = char;
|
|
1258
1936
|
continue;
|
|
1259
1937
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
tokens.push("&&");
|
|
1263
|
-
index += 1;
|
|
1264
|
-
continue;
|
|
1265
|
-
}
|
|
1266
|
-
if (char === "|" && next === "|") {
|
|
1267
|
-
flush();
|
|
1268
|
-
tokens.push("||");
|
|
1269
|
-
index += 1;
|
|
1270
|
-
continue;
|
|
1271
|
-
}
|
|
1272
|
-
if (char === ">" && next === ">") {
|
|
1273
|
-
flush();
|
|
1274
|
-
tokens.push(">>");
|
|
1275
|
-
index += 1;
|
|
1276
|
-
continue;
|
|
1277
|
-
}
|
|
1278
|
-
if (char === "|" || char === ";" || char === ">" || char === "<") {
|
|
1938
|
+
const operator = readShellOperator(input, index);
|
|
1939
|
+
if (operator) {
|
|
1279
1940
|
flush();
|
|
1280
|
-
tokens.push(
|
|
1941
|
+
tokens.push(operator.token);
|
|
1942
|
+
index += operator.length - 1;
|
|
1281
1943
|
continue;
|
|
1282
1944
|
}
|
|
1283
1945
|
if (char === "\n" || char === "\r") {
|
|
@@ -1307,7 +1969,10 @@ function extractRedirectTargets(tokens) {
|
|
|
1307
1969
|
const targets = [];
|
|
1308
1970
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
1309
1971
|
const token = tokens[index];
|
|
1310
|
-
if (token
|
|
1972
|
+
if (isFdDuplication(token)) {
|
|
1973
|
+
continue;
|
|
1974
|
+
}
|
|
1975
|
+
if (isRedirectOperator(token)) {
|
|
1311
1976
|
const next = tokens[index + 1];
|
|
1312
1977
|
if (next) {
|
|
1313
1978
|
targets.push(next);
|
|
@@ -1317,73 +1982,215 @@ function extractRedirectTargets(tokens) {
|
|
|
1317
1982
|
return targets;
|
|
1318
1983
|
}
|
|
1319
1984
|
|
|
1320
|
-
// src/core/
|
|
1321
|
-
function
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1985
|
+
// src/core/path-utils.ts
|
|
1986
|
+
function canonicalPath(targetPath) {
|
|
1987
|
+
const resolved = path10.resolve(targetPath);
|
|
1988
|
+
if (!resolved) {
|
|
1989
|
+
return resolved;
|
|
1990
|
+
}
|
|
1991
|
+
const parsed = path10.parse(resolved);
|
|
1992
|
+
let current = parsed.root;
|
|
1993
|
+
const relativeParts = path10.relative(parsed.root || ".", resolved).split(path10.sep).filter(Boolean);
|
|
1994
|
+
for (let i = 0; i < relativeParts.length; i++) {
|
|
1995
|
+
const segment = relativeParts[i];
|
|
1996
|
+
if (!segment) {
|
|
1327
1997
|
continue;
|
|
1328
1998
|
}
|
|
1329
|
-
const
|
|
1330
|
-
if (
|
|
1331
|
-
|
|
1999
|
+
const candidate = current === "" ? segment : path10.join(current, segment);
|
|
2000
|
+
if (!existsSync2(candidate)) {
|
|
2001
|
+
return path10.join(candidate, ...relativeParts.slice(i + 1));
|
|
2002
|
+
}
|
|
2003
|
+
try {
|
|
2004
|
+
current = realpathSync.native(candidate);
|
|
2005
|
+
} catch {
|
|
2006
|
+
return path10.join(candidate, ...relativeParts.slice(i + 1));
|
|
1332
2007
|
}
|
|
1333
2008
|
}
|
|
1334
|
-
return
|
|
2009
|
+
return current;
|
|
1335
2010
|
}
|
|
1336
|
-
function
|
|
1337
|
-
const
|
|
1338
|
-
|
|
2011
|
+
function pathWithinRoot(root, targetPath) {
|
|
2012
|
+
const resolvedRoot = canonicalPath(root);
|
|
2013
|
+
const resolvedTarget = canonicalPath(targetPath);
|
|
2014
|
+
const relativePath = path10.relative(resolvedRoot, resolvedTarget);
|
|
2015
|
+
if (relativePath === "") {
|
|
2016
|
+
return true;
|
|
2017
|
+
}
|
|
2018
|
+
return !relativePath.startsWith("..") && !path10.isAbsolute(relativePath);
|
|
2019
|
+
}
|
|
2020
|
+
function relativeWithinRoot(root, targetPath, options = {}) {
|
|
2021
|
+
const resolvedRoot = options.canonicalizeRoot === false ? path10.resolve(root) : canonicalPath(root);
|
|
2022
|
+
const resolvedTarget = canonicalPath(targetPath);
|
|
2023
|
+
const relativePath = path10.relative(resolvedRoot, resolvedTarget);
|
|
2024
|
+
if (relativePath === "") {
|
|
2025
|
+
return ".";
|
|
2026
|
+
}
|
|
2027
|
+
if (relativePath.startsWith("..") || path10.isAbsolute(relativePath)) {
|
|
1339
2028
|
return null;
|
|
1340
2029
|
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
2030
|
+
return relativePath;
|
|
2031
|
+
}
|
|
2032
|
+
function relativeWithinRepo(repoRoot, targetPath) {
|
|
2033
|
+
return relativeWithinRoot(repoRoot, targetPath);
|
|
2034
|
+
}
|
|
2035
|
+
function resolveWorkspaceRootMatch(repoRoot, trustedRoots = [], targetPath) {
|
|
2036
|
+
const canonicalRepoRoot = canonicalPath(repoRoot);
|
|
2037
|
+
const repoRelative = relativeWithinRoot(repoRoot, targetPath);
|
|
2038
|
+
if (repoRelative !== null) {
|
|
2039
|
+
return { kind: "repo", root: canonicalRepoRoot, relativePath: repoRelative };
|
|
2040
|
+
}
|
|
2041
|
+
const normalizedTrustedRoots = [...new Set(trustedRoots.map((root) => path10.resolve(root)))].filter((root) => root !== canonicalRepoRoot).sort((left, right) => right.length - left.length);
|
|
2042
|
+
for (const root of normalizedTrustedRoots) {
|
|
2043
|
+
const trustedRelative = relativeWithinRoot(root, targetPath, { canonicalizeRoot: false });
|
|
2044
|
+
if (trustedRelative !== null) {
|
|
2045
|
+
return { kind: "trusted", root, relativePath: trustedRelative };
|
|
1345
2046
|
}
|
|
1346
2047
|
}
|
|
1347
2048
|
return null;
|
|
1348
2049
|
}
|
|
1349
|
-
function
|
|
1350
|
-
|
|
1351
|
-
if (!toolInput || typeof toolInput !== "object") {
|
|
2050
|
+
function resolveMutationTarget(token, cwd) {
|
|
2051
|
+
if (!token || token === "--" || token.startsWith("-")) {
|
|
1352
2052
|
return null;
|
|
1353
2053
|
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
if (typeof input[key] === "string" && input[key].trim()) {
|
|
1357
|
-
return input[key];
|
|
1358
|
-
}
|
|
2054
|
+
if (isRedirectOperator(token) || isFdDuplication(token) || token === "&&" || token === "||" || token === "|" || token === "|&" || token === ";" || token === "&") {
|
|
2055
|
+
return null;
|
|
1359
2056
|
}
|
|
1360
|
-
|
|
2057
|
+
if (path10.isAbsolute(token)) {
|
|
2058
|
+
return canonicalPath(token);
|
|
2059
|
+
}
|
|
2060
|
+
if (token.startsWith("./") || token.startsWith("../")) {
|
|
2061
|
+
return canonicalPath(path10.resolve(cwd, token));
|
|
2062
|
+
}
|
|
2063
|
+
if (!token.includes("/") && !token.includes("\\")) {
|
|
2064
|
+
return canonicalPath(path10.resolve(cwd, token));
|
|
2065
|
+
}
|
|
2066
|
+
return canonicalPath(path10.resolve(cwd, token));
|
|
2067
|
+
}
|
|
2068
|
+
function containingGitRoot(targetPath) {
|
|
2069
|
+
const resolved = canonicalPath(targetPath);
|
|
2070
|
+
let current = resolved;
|
|
2071
|
+
try {
|
|
2072
|
+
if (!statSync(current).isDirectory()) {
|
|
2073
|
+
current = path10.dirname(current);
|
|
2074
|
+
}
|
|
2075
|
+
} catch {
|
|
2076
|
+
current = path10.dirname(current);
|
|
2077
|
+
}
|
|
2078
|
+
while (true) {
|
|
2079
|
+
if (existsSync2(path10.join(current, ".git"))) {
|
|
2080
|
+
return current;
|
|
2081
|
+
}
|
|
2082
|
+
const parent = path10.dirname(current);
|
|
2083
|
+
if (parent === current) {
|
|
2084
|
+
return null;
|
|
2085
|
+
}
|
|
2086
|
+
current = parent;
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
// src/core/capability/allowlist.ts
|
|
2091
|
+
function fsScopeAllowlistPath(config, repoLocalStateDir) {
|
|
2092
|
+
return path11.join(belayStateDir(config, repoLocalStateDir), "fs-scope-allowlist.json");
|
|
2093
|
+
}
|
|
2094
|
+
function loadFsScopeAllowlistSync(filePath) {
|
|
2095
|
+
if (!existsSync3(filePath)) {
|
|
2096
|
+
return { version: 1, paths: [] };
|
|
2097
|
+
}
|
|
2098
|
+
const raw = JSON.parse(readFileSync(filePath, "utf8"));
|
|
2099
|
+
return {
|
|
2100
|
+
version: 1,
|
|
2101
|
+
paths: Array.isArray(raw.paths) ? raw.paths : []
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
function normalizeAllowlistPath(targetPath) {
|
|
2105
|
+
return canonicalPath(targetPath);
|
|
2106
|
+
}
|
|
2107
|
+
function isPathAllowlisted(absolutePath, allowlist) {
|
|
2108
|
+
const resolved = normalizeAllowlistPath(absolutePath);
|
|
2109
|
+
return allowlist.paths.some((entry) => {
|
|
2110
|
+
const scope = normalizeAllowlistPath(entry.path);
|
|
2111
|
+
return resolved === scope || pathWithinRoot(scope, resolved);
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
function allPathsAllowlisted(absolutePaths, allowlist) {
|
|
2115
|
+
return absolutePaths.length > 0 && absolutePaths.every((entry) => isPathAllowlisted(entry, allowlist));
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
// src/core/capability/broker.ts
|
|
2119
|
+
function hasSandboxRuntime(config) {
|
|
2120
|
+
return config.sandbox.enabled && config.sandbox.runtime !== "none";
|
|
2121
|
+
}
|
|
2122
|
+
function isCapabilityBrokerDemotionActive(config) {
|
|
2123
|
+
return hasSandboxRuntime(config);
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
// src/core/capability/paths.ts
|
|
2127
|
+
import path12 from "node:path";
|
|
2128
|
+
function applyPatchTargets(patch) {
|
|
2129
|
+
const targets = [];
|
|
2130
|
+
for (const line of patch.split("\n")) {
|
|
2131
|
+
const match = line.match(/^\*\*\* (?:Add|Delete|Update) File: (.+)$/);
|
|
2132
|
+
if (match?.[1]) {
|
|
2133
|
+
targets.push(match[1]);
|
|
2134
|
+
continue;
|
|
2135
|
+
}
|
|
2136
|
+
const moveMatch = line.match(/^\*\*\* Move to: (.+)$/);
|
|
2137
|
+
if (moveMatch?.[1]) {
|
|
2138
|
+
targets.push(moveMatch[1]);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
return targets;
|
|
2142
|
+
}
|
|
2143
|
+
function extractToolFilePath(payload) {
|
|
2144
|
+
const toolInput = payload.tool_input;
|
|
2145
|
+
if (!toolInput || typeof toolInput !== "object") {
|
|
2146
|
+
return null;
|
|
2147
|
+
}
|
|
2148
|
+
const input = toolInput;
|
|
2149
|
+
for (const key of ["path", "file_path", "target_file", "filePath"]) {
|
|
2150
|
+
if (typeof input[key] === "string") {
|
|
2151
|
+
return input[key];
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
return null;
|
|
2155
|
+
}
|
|
2156
|
+
function extractToolPatch(payload) {
|
|
2157
|
+
const toolInput = payload.tool_input;
|
|
2158
|
+
if (!toolInput || typeof toolInput !== "object") {
|
|
2159
|
+
return null;
|
|
2160
|
+
}
|
|
2161
|
+
const input = toolInput;
|
|
2162
|
+
for (const key of ["patch", "input", "text"]) {
|
|
2163
|
+
if (typeof input[key] === "string" && input[key].trim()) {
|
|
2164
|
+
return input[key];
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
return null;
|
|
1361
2168
|
}
|
|
1362
|
-
function addOutsideRepoPath(paths, token, cwd, repoRoot) {
|
|
2169
|
+
function addOutsideRepoPath(paths, token, cwd, repoRoot, trustedWorkspaceRoots = []) {
|
|
1363
2170
|
const resolved = resolveMutationTarget(token, cwd);
|
|
1364
|
-
if (resolved &&
|
|
2171
|
+
if (resolved && resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolved) === null) {
|
|
1365
2172
|
paths.add(resolved);
|
|
1366
2173
|
}
|
|
1367
2174
|
}
|
|
1368
|
-
function collectOutsideRepoPaths(command, cwd, repoRoot) {
|
|
2175
|
+
function collectOutsideRepoPaths(command, cwd, repoRoot, trustedWorkspaceRoots = []) {
|
|
1369
2176
|
const tokens = tokenizeShell(command);
|
|
1370
2177
|
const redirects = extractRedirectTargets(tokens);
|
|
1371
2178
|
const paths = /* @__PURE__ */ new Set();
|
|
1372
2179
|
for (const token of tokens.slice(1)) {
|
|
1373
|
-
addOutsideRepoPath(paths, token, cwd, repoRoot);
|
|
2180
|
+
addOutsideRepoPath(paths, token, cwd, repoRoot, trustedWorkspaceRoots);
|
|
1374
2181
|
}
|
|
1375
2182
|
for (const redirect of redirects) {
|
|
1376
|
-
addOutsideRepoPath(paths, redirect, cwd, repoRoot);
|
|
2183
|
+
addOutsideRepoPath(paths, redirect, cwd, repoRoot, trustedWorkspaceRoots);
|
|
1377
2184
|
}
|
|
1378
2185
|
return [...paths];
|
|
1379
2186
|
}
|
|
1380
|
-
function collectOutsideRepoPathsFromToolPayload(payload, cwd, repoRoot) {
|
|
2187
|
+
function collectOutsideRepoPathsFromToolPayload(payload, cwd, repoRoot, trustedWorkspaceRoots = []) {
|
|
1381
2188
|
const toolKind = String(payload.tool_name ?? "").trim().toLowerCase();
|
|
1382
2189
|
const paths = /* @__PURE__ */ new Set();
|
|
1383
2190
|
const filePath = extractToolFilePath(payload);
|
|
1384
2191
|
if (filePath) {
|
|
1385
|
-
const resolved =
|
|
1386
|
-
if (
|
|
2192
|
+
const resolved = path12.isAbsolute(filePath) ? filePath : path12.resolve(cwd, filePath);
|
|
2193
|
+
if (resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolved) === null) {
|
|
1387
2194
|
paths.add(resolved);
|
|
1388
2195
|
}
|
|
1389
2196
|
return [...paths];
|
|
@@ -1392,8 +2199,8 @@ function collectOutsideRepoPathsFromToolPayload(payload, cwd, repoRoot) {
|
|
|
1392
2199
|
const patch = extractToolPatch(payload);
|
|
1393
2200
|
if (patch) {
|
|
1394
2201
|
for (const target of applyPatchTargets(patch)) {
|
|
1395
|
-
const resolved =
|
|
1396
|
-
if (
|
|
2202
|
+
const resolved = path12.isAbsolute(target) ? target : path12.resolve(cwd, target);
|
|
2203
|
+
if (resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolved) === null) {
|
|
1397
2204
|
paths.add(resolved);
|
|
1398
2205
|
}
|
|
1399
2206
|
}
|
|
@@ -1416,186 +2223,123 @@ function shouldSkipBrokerApprovedRecord(brokerActive, approvalReason) {
|
|
|
1416
2223
|
return brokerActive && approvalReason !== void 0 && FS_SCOPE_REASONS.has(approvalReason);
|
|
1417
2224
|
}
|
|
1418
2225
|
|
|
1419
|
-
// src/core/
|
|
1420
|
-
import
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
unknownLocalEffect: "deny",
|
|
1429
|
-
unparseableShell: "deny",
|
|
1430
|
-
confidenceThresholds: { allow: 0.9, flag: 0.8 },
|
|
1431
|
-
modelAssist: { enabled: false }
|
|
1432
|
-
},
|
|
1433
|
-
sandbox: { ...DEFAULT_CONFIG_V3.sandbox }
|
|
1434
|
-
},
|
|
1435
|
-
standard: {
|
|
1436
|
-
mode: "enforce"
|
|
1437
|
-
},
|
|
1438
|
-
"audit-first": {
|
|
1439
|
-
mode: "audit",
|
|
1440
|
-
policy: {
|
|
1441
|
-
...DEFAULT_CONFIG_V3.policy,
|
|
1442
|
-
unknownLocalEffect: "deny",
|
|
1443
|
-
unparseableShell: "deny",
|
|
1444
|
-
confidenceThresholds: { allow: 0.88, flag: 0.72 },
|
|
1445
|
-
modelAssist: { enabled: false }
|
|
1446
|
-
},
|
|
1447
|
-
sandbox: { ...DEFAULT_CONFIG_V3.sandbox }
|
|
1448
|
-
},
|
|
1449
|
-
"l1-full-recommended": {
|
|
1450
|
-
mode: "enforce",
|
|
1451
|
-
policy: {
|
|
1452
|
-
...DEFAULT_CONFIG_V3.policy,
|
|
1453
|
-
confidenceThresholds: { ...DEFAULT_CONFIG_V3.policy.confidenceThresholds },
|
|
1454
|
-
modelAssist: { ...DEFAULT_CONFIG_V3.policy.modelAssist }
|
|
1455
|
-
},
|
|
1456
|
-
sandbox: {
|
|
1457
|
-
enabled: true,
|
|
1458
|
-
runtime: "container",
|
|
1459
|
-
denyNetworkByDefault: true
|
|
1460
|
-
},
|
|
1461
|
-
egress: {
|
|
1462
|
-
...DEFAULT_CONFIG_V3.egress,
|
|
1463
|
-
enabled: true,
|
|
1464
|
-
demoteL3External: true
|
|
1465
|
-
},
|
|
1466
|
-
approvalSigning: {
|
|
1467
|
-
required: true
|
|
1468
|
-
},
|
|
1469
|
-
controlPlane: {
|
|
1470
|
-
...DEFAULT_CONFIG_V3.controlPlane,
|
|
1471
|
-
isolation: {
|
|
1472
|
-
mode: "separate-user",
|
|
1473
|
-
verifyAgentWritable: true
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
2226
|
+
// src/core/capability/trusted-workspace-roots.ts
|
|
2227
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
|
|
2228
|
+
import path13 from "node:path";
|
|
2229
|
+
function trustedWorkspaceRootsPath(config, repoLocalStateDir) {
|
|
2230
|
+
return path13.join(belayStateDir(config, repoLocalStateDir), "trusted-workspace-roots.json");
|
|
2231
|
+
}
|
|
2232
|
+
function loadTrustedWorkspaceRootsSync(filePath) {
|
|
2233
|
+
if (!existsSync4(filePath)) {
|
|
2234
|
+
return { version: 1, roots: [] };
|
|
1476
2235
|
}
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
const base = CONFIG_PRESETS[preset] ?? CONFIG_PRESETS.standard;
|
|
1480
|
-
return {
|
|
1481
|
-
version: 3,
|
|
1482
|
-
...base,
|
|
1483
|
-
...extra,
|
|
1484
|
-
policy: {
|
|
1485
|
-
...DEFAULT_CONFIG_V3.policy,
|
|
1486
|
-
...base.policy ?? {},
|
|
1487
|
-
...extra.policy
|
|
1488
|
-
},
|
|
1489
|
-
sandbox: {
|
|
1490
|
-
...DEFAULT_CONFIG_V3.sandbox,
|
|
1491
|
-
...base.sandbox ?? {},
|
|
1492
|
-
...extra.sandbox
|
|
1493
|
-
},
|
|
1494
|
-
egress: {
|
|
1495
|
-
...DEFAULT_CONFIG_V3.egress,
|
|
1496
|
-
...base.egress ?? {},
|
|
1497
|
-
...extra.egress
|
|
1498
|
-
},
|
|
1499
|
-
approvalSigning: {
|
|
1500
|
-
...DEFAULT_CONFIG_V3.approvalSigning,
|
|
1501
|
-
...base.approvalSigning ?? {},
|
|
1502
|
-
...extra.approvalSigning
|
|
1503
|
-
},
|
|
1504
|
-
controlPlane: {
|
|
1505
|
-
...DEFAULT_CONFIG_V3.controlPlane,
|
|
1506
|
-
...base.controlPlane ?? {},
|
|
1507
|
-
...extra.controlPlane,
|
|
1508
|
-
isolation: {
|
|
1509
|
-
...DEFAULT_CONFIG_V3.controlPlane.isolation,
|
|
1510
|
-
...base.controlPlane?.isolation ?? {},
|
|
1511
|
-
...extra.controlPlane?.isolation ?? {}
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
};
|
|
2236
|
+
const raw = JSON.parse(readFileSync2(filePath, "utf8"));
|
|
2237
|
+
return { version: 1, roots: sanitizeTrustedWorkspaceRootEntries(raw.roots) };
|
|
1515
2238
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
function teamConfigPath(homedir = () => process.env.HOME ?? process.env.USERPROFILE ?? "") {
|
|
1519
|
-
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
1520
|
-
const base = xdg || path8.join(homedir(), ".config");
|
|
1521
|
-
return path8.join(base, "agent-belay", "team.config.json");
|
|
2239
|
+
function normalizeTrustedWorkspaceRootPath(targetPath) {
|
|
2240
|
+
return canonicalPath(targetPath);
|
|
1522
2241
|
}
|
|
1523
|
-
function
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
controlPlane.enabled = true;
|
|
1527
|
-
}
|
|
1528
|
-
if (builtin.controlPlane.integrity === "hash-pinned" && controlPlane.integrity === "none") {
|
|
1529
|
-
controlPlane.integrity = "hash-pinned";
|
|
2242
|
+
function sanitizeTrustedWorkspaceRootEntries(input) {
|
|
2243
|
+
if (!Array.isArray(input)) {
|
|
2244
|
+
return [];
|
|
1530
2245
|
}
|
|
1531
|
-
return {
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
2246
|
+
return input.flatMap((entry) => {
|
|
2247
|
+
if (!entry || typeof entry !== "object") {
|
|
2248
|
+
return [];
|
|
2249
|
+
}
|
|
2250
|
+
const record = entry;
|
|
2251
|
+
if (typeof record.path !== "string" || !record.path.trim()) {
|
|
2252
|
+
return [];
|
|
2253
|
+
}
|
|
2254
|
+
const approvedAt = typeof record.approvedAt === "string" ? record.approvedAt : (/* @__PURE__ */ new Date(0)).toISOString();
|
|
2255
|
+
const approvalId = typeof record.approvalId === "string" ? record.approvalId : "unknown";
|
|
2256
|
+
const source = record.source === "approval" ? "approval" : void 0;
|
|
2257
|
+
return [
|
|
2258
|
+
{
|
|
2259
|
+
path: path13.resolve(record.path),
|
|
2260
|
+
approvedAt,
|
|
2261
|
+
approvalId,
|
|
2262
|
+
...source ? { source } : {}
|
|
2263
|
+
}
|
|
2264
|
+
];
|
|
2265
|
+
});
|
|
1535
2266
|
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
2267
|
+
var SYSTEM_HIGH_STAKES_PREFIXES = [
|
|
2268
|
+
"/etc",
|
|
2269
|
+
"/private/etc",
|
|
2270
|
+
"/bin",
|
|
2271
|
+
"/sbin",
|
|
2272
|
+
"/usr",
|
|
2273
|
+
"/opt",
|
|
2274
|
+
"/System",
|
|
2275
|
+
"/Library"
|
|
2276
|
+
];
|
|
2277
|
+
var HOME_HIGH_STAKES_SEGMENTS = [".ssh", ".gnupg", ".aws", ".kube", ".docker", ".config"];
|
|
2278
|
+
function isDirectoryPath(targetPath) {
|
|
2279
|
+
try {
|
|
2280
|
+
return statSync2(targetPath).isDirectory();
|
|
2281
|
+
} catch {
|
|
2282
|
+
return false;
|
|
1539
2283
|
}
|
|
1540
|
-
return { version: 3, ...raw };
|
|
1541
2284
|
}
|
|
1542
|
-
function
|
|
1543
|
-
const
|
|
1544
|
-
|
|
1545
|
-
|
|
2285
|
+
function isBroadTrustedWorkspaceRoot(targetPath) {
|
|
2286
|
+
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
2287
|
+
const root = normalizeTrustedWorkspaceRootPath(targetPath);
|
|
2288
|
+
if (root === normalizeTrustedWorkspaceRootPath(path13.parse(root).root)) {
|
|
2289
|
+
return true;
|
|
1546
2290
|
}
|
|
1547
|
-
|
|
2291
|
+
if (home && normalizeTrustedWorkspaceRootPath(home) === root) {
|
|
2292
|
+
return true;
|
|
2293
|
+
}
|
|
2294
|
+
return false;
|
|
1548
2295
|
}
|
|
1549
|
-
function
|
|
1550
|
-
const
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
rejectTeamLayerJudgeSecrets(
|
|
1556
|
-
teamRaw.judge,
|
|
1557
|
-
"team"
|
|
1558
|
-
);
|
|
1559
|
-
config = mergeConfigLayer(config, asV3Layer(teamRaw));
|
|
1560
|
-
provenance.push({
|
|
1561
|
-
path: params.teamConfigPath ?? teamConfigPath(),
|
|
1562
|
-
source: "team"
|
|
1563
|
-
});
|
|
2296
|
+
function isHighStakesTrustedWorkspaceRoot(targetPath) {
|
|
2297
|
+
const normalized = normalizeTrustedWorkspaceRootPath(targetPath);
|
|
2298
|
+
if (SYSTEM_HIGH_STAKES_PREFIXES.some(
|
|
2299
|
+
(prefix) => normalized === normalizeTrustedWorkspaceRootPath(prefix) || pathWithinRoot(normalizeTrustedWorkspaceRootPath(prefix), normalized)
|
|
2300
|
+
)) {
|
|
2301
|
+
return true;
|
|
1564
2302
|
}
|
|
1565
|
-
|
|
1566
|
-
if (
|
|
1567
|
-
|
|
2303
|
+
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
2304
|
+
if (!home) {
|
|
2305
|
+
return false;
|
|
1568
2306
|
}
|
|
1569
|
-
const
|
|
1570
|
-
if (
|
|
1571
|
-
|
|
1572
|
-
config = protectedConfig;
|
|
2307
|
+
const homeRoot = normalizeTrustedWorkspaceRootPath(home);
|
|
2308
|
+
if (!pathWithinRoot(homeRoot, normalized)) {
|
|
2309
|
+
return false;
|
|
1573
2310
|
}
|
|
1574
|
-
return
|
|
2311
|
+
return HOME_HIGH_STAKES_SEGMENTS.some(
|
|
2312
|
+
(segment) => pathWithinRoot(path13.join(homeRoot, segment), normalized)
|
|
2313
|
+
);
|
|
1575
2314
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
if (value === null || typeof value !== "object") {
|
|
1581
|
-
return JSON.stringify(value);
|
|
2315
|
+
function validateTrustedWorkspaceRootCandidate(params) {
|
|
2316
|
+
const normalizedPath = normalizeTrustedWorkspaceRootPath(params.candidatePath);
|
|
2317
|
+
if (params.requireExistingDirectory !== false && !isDirectoryPath(normalizedPath)) {
|
|
2318
|
+
return { ok: false, normalizedPath, reason: "not_directory" };
|
|
1582
2319
|
}
|
|
1583
|
-
if (
|
|
1584
|
-
return
|
|
2320
|
+
if (isBroadTrustedWorkspaceRoot(normalizedPath)) {
|
|
2321
|
+
return { ok: false, normalizedPath, reason: "broad_root" };
|
|
1585
2322
|
}
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
2323
|
+
if (isHighStakesTrustedWorkspaceRoot(normalizedPath)) {
|
|
2324
|
+
return { ok: false, normalizedPath, reason: "high_stakes" };
|
|
2325
|
+
}
|
|
2326
|
+
if (resolveWorkspaceRootMatch(params.repoRoot, [], normalizedPath) !== null) {
|
|
2327
|
+
return { ok: false, normalizedPath, reason: "inside_repo" };
|
|
2328
|
+
}
|
|
2329
|
+
if (params.requireNonGit !== false && containingGitRoot(normalizedPath)) {
|
|
2330
|
+
return { ok: false, normalizedPath, reason: "inside_git_repo" };
|
|
2331
|
+
}
|
|
2332
|
+
const normalizedControlPlane = params.controlPlaneDir?.trim() ? normalizeTrustedWorkspaceRootPath(params.controlPlaneDir) : null;
|
|
2333
|
+
if (normalizedControlPlane && (pathWithinRoot(normalizedControlPlane, normalizedPath) || pathWithinRoot(normalizedPath, normalizedControlPlane))) {
|
|
2334
|
+
return { ok: false, normalizedPath, reason: "control_plane_overlap" };
|
|
2335
|
+
}
|
|
2336
|
+
if ((params.protectedRoots ?? []).some((root) => {
|
|
2337
|
+
const normalizedRoot = normalizeTrustedWorkspaceRootPath(root);
|
|
2338
|
+
return pathWithinRoot(normalizedRoot, normalizedPath) || pathWithinRoot(normalizedPath, normalizedRoot);
|
|
2339
|
+
})) {
|
|
2340
|
+
return { ok: false, normalizedPath, reason: "protected_root_overlap" };
|
|
2341
|
+
}
|
|
2342
|
+
return { ok: true, normalizedPath };
|
|
1599
2343
|
}
|
|
1600
2344
|
|
|
1601
2345
|
// src/core/scrub.ts
|
|
@@ -1683,6 +2427,51 @@ function scrubValue(value, options = DEFAULT_SCRUB_OPTIONS) {
|
|
|
1683
2427
|
return value;
|
|
1684
2428
|
}
|
|
1685
2429
|
|
|
2430
|
+
// src/core/replay-scrub.ts
|
|
2431
|
+
function subagentFingerprintSource(payload, scrubOptions) {
|
|
2432
|
+
const toolInput = payload.tool_input;
|
|
2433
|
+
if (toolInput && typeof toolInput === "object") {
|
|
2434
|
+
const input = toolInput;
|
|
2435
|
+
return scrubValue(
|
|
2436
|
+
{
|
|
2437
|
+
description: input.description ?? "",
|
|
2438
|
+
prompt: input.prompt ?? ""
|
|
2439
|
+
},
|
|
2440
|
+
scrubOptions
|
|
2441
|
+
);
|
|
2442
|
+
}
|
|
2443
|
+
const task = payload.task;
|
|
2444
|
+
if (typeof task === "string") {
|
|
2445
|
+
return scrubValue({ task }, scrubOptions);
|
|
2446
|
+
}
|
|
2447
|
+
if (task && typeof task === "object") {
|
|
2448
|
+
const taskObj = task;
|
|
2449
|
+
return scrubValue(
|
|
2450
|
+
{
|
|
2451
|
+
description: taskObj.description ?? "",
|
|
2452
|
+
prompt: taskObj.prompt ?? ""
|
|
2453
|
+
},
|
|
2454
|
+
scrubOptions
|
|
2455
|
+
);
|
|
2456
|
+
}
|
|
2457
|
+
return scrubValue(payload, scrubOptions);
|
|
2458
|
+
}
|
|
2459
|
+
function fingerprintReplayPayload(kind, payload, scrubOptions) {
|
|
2460
|
+
if (!payload) {
|
|
2461
|
+
return void 0;
|
|
2462
|
+
}
|
|
2463
|
+
if (kind === "tool") {
|
|
2464
|
+
const toolInput = payload.tool_input;
|
|
2465
|
+
if (toolInput && typeof toolInput === "object") {
|
|
2466
|
+
return scrubValue(toolInput, scrubOptions);
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
if (kind === "subagent") {
|
|
2470
|
+
return subagentFingerprintSource(payload, scrubOptions);
|
|
2471
|
+
}
|
|
2472
|
+
return scrubValue(payload, scrubOptions);
|
|
2473
|
+
}
|
|
2474
|
+
|
|
1686
2475
|
// src/core/classify-subagent.ts
|
|
1687
2476
|
var EXTERNAL_TERMS = ["deploy", "production", "publish", "release", "ship", "notify", "email"];
|
|
1688
2477
|
function extractSubagentText(payload, options) {
|
|
@@ -1706,32 +2495,7 @@ function extractSubagentText(payload, options) {
|
|
|
1706
2495
|
return canonicalStringify(scrubValue(payload, options.scrubOptions));
|
|
1707
2496
|
}
|
|
1708
2497
|
function fingerprintSource(payload, options) {
|
|
1709
|
-
|
|
1710
|
-
if (toolInput && typeof toolInput === "object") {
|
|
1711
|
-
const input = toolInput;
|
|
1712
|
-
return scrubValue(
|
|
1713
|
-
{
|
|
1714
|
-
description: input.description ?? "",
|
|
1715
|
-
prompt: input.prompt ?? ""
|
|
1716
|
-
},
|
|
1717
|
-
options.scrubOptions
|
|
1718
|
-
);
|
|
1719
|
-
}
|
|
1720
|
-
const task = payload.task;
|
|
1721
|
-
if (typeof task === "string") {
|
|
1722
|
-
return scrubValue({ task }, options.scrubOptions);
|
|
1723
|
-
}
|
|
1724
|
-
if (task && typeof task === "object") {
|
|
1725
|
-
const taskObj = task;
|
|
1726
|
-
return scrubValue(
|
|
1727
|
-
{
|
|
1728
|
-
description: taskObj.description ?? "",
|
|
1729
|
-
prompt: taskObj.prompt ?? ""
|
|
1730
|
-
},
|
|
1731
|
-
options.scrubOptions
|
|
1732
|
-
);
|
|
1733
|
-
}
|
|
1734
|
-
return scrubValue(payload, options.scrubOptions);
|
|
2498
|
+
return subagentFingerprintSource(payload, options.scrubOptions ?? {});
|
|
1735
2499
|
}
|
|
1736
2500
|
function classifySubagent(payload, repoRoot, options = {}) {
|
|
1737
2501
|
const kind = payload.tool_name === "Task" ? "Task" : String(payload.subagent_type ?? "generalPurpose");
|
|
@@ -1759,7 +2523,7 @@ function classifySubagent(payload, repoRoot, options = {}) {
|
|
|
1759
2523
|
}
|
|
1760
2524
|
|
|
1761
2525
|
// src/core/classify-tool.ts
|
|
1762
|
-
import
|
|
2526
|
+
import path21 from "node:path";
|
|
1763
2527
|
|
|
1764
2528
|
// src/core/glob.ts
|
|
1765
2529
|
function matchesSensitivePath(filePath, patterns) {
|
|
@@ -1805,6 +2569,10 @@ function matchesSensitivePath(filePath, patterns) {
|
|
|
1805
2569
|
}
|
|
1806
2570
|
|
|
1807
2571
|
// src/core/verdict/judge-audit.ts
|
|
2572
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2573
|
+
function hashJudgeSessionRef(value) {
|
|
2574
|
+
return createHash3("sha256").update(value).digest("hex").slice(0, 16);
|
|
2575
|
+
}
|
|
1808
2576
|
function judgeTraceAuditFields(trace) {
|
|
1809
2577
|
if (!trace) {
|
|
1810
2578
|
return {};
|
|
@@ -1815,121 +2583,40 @@ function judgeTraceAuditFields(trace) {
|
|
|
1815
2583
|
judgeModelResolved: trace.modelResolved,
|
|
1816
2584
|
judgeLatencyMs: trace.latencyMs,
|
|
1817
2585
|
...trace.outboundRedacted !== void 0 ? { judgeOutboundRedacted: trace.outboundRedacted } : {},
|
|
1818
|
-
...trace.fallbackReason ? { judgeFallbackReason: trace.fallbackReason } : {}
|
|
2586
|
+
...trace.judgeFallbackReason ? { judgeFallbackReason: trace.judgeFallbackReason } : trace.fallbackReason ? { judgeFallbackReason: trace.fallbackReason } : {},
|
|
2587
|
+
...trace.judgeSessionUsed !== void 0 ? { judgeSessionUsed: trace.judgeSessionUsed } : {},
|
|
2588
|
+
...trace.judgeSessionReused !== void 0 ? { judgeSessionReused: trace.judgeSessionReused } : {},
|
|
2589
|
+
...trace.judgeSessionRefHash ? { judgeSessionRefHash: trace.judgeSessionRefHash } : {},
|
|
2590
|
+
...trace.judgeSessionResetReason ? { judgeSessionResetReason: trace.judgeSessionResetReason } : {},
|
|
2591
|
+
...trace.judgeConnectMs !== void 0 ? { judgeConnectMs: trace.judgeConnectMs } : {},
|
|
2592
|
+
...trace.judgeEvalMs !== void 0 ? { judgeEvalMs: trace.judgeEvalMs } : {},
|
|
2593
|
+
...trace.judgeParseMs !== void 0 ? { judgeParseMs: trace.judgeParseMs } : {},
|
|
2594
|
+
...trace.judgeShadowCompared !== void 0 ? { judgeShadowCompared: trace.judgeShadowCompared } : {},
|
|
2595
|
+
...trace.judgeShadowMismatch !== void 0 ? { judgeShadowMismatch: trace.judgeShadowMismatch } : {},
|
|
2596
|
+
...trace.judgeShadowMismatchRateWindow !== void 0 ? { judgeShadowMismatchRateWindow: trace.judgeShadowMismatchRateWindow } : {},
|
|
2597
|
+
...trace.judgeKillSwitchTriggered !== void 0 ? { judgeKillSwitchTriggered: trace.judgeKillSwitchTriggered } : {}
|
|
1819
2598
|
};
|
|
1820
2599
|
}
|
|
1821
2600
|
|
|
1822
|
-
// src/
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
var codexLayout = {
|
|
1829
|
-
name: "codex",
|
|
1830
|
-
configPath(repoRoot) {
|
|
1831
|
-
return path9.join(repoRoot, ".codex", "belay.config.json");
|
|
1832
|
-
},
|
|
1833
|
-
// Codex reads lifecycle hooks from `.codex/config.toml` (project layer).
|
|
1834
|
-
hooksSettingsPath(repoRoot) {
|
|
1835
|
-
return path9.join(repoRoot, ".codex", "config.toml");
|
|
1836
|
-
},
|
|
1837
|
-
hooksDir(repoRoot) {
|
|
1838
|
-
return path9.join(repoRoot, ".codex", "hooks");
|
|
1839
|
-
},
|
|
1840
|
-
runtimeDir(repoRoot) {
|
|
1841
|
-
return path9.join(repoRoot, ".codex", "belay", "runtime");
|
|
1842
|
-
},
|
|
1843
|
-
repoLocalStateDir(repoRoot) {
|
|
1844
|
-
return path9.join(repoRoot, ".codex", "belay");
|
|
1845
|
-
},
|
|
1846
|
-
defaultAuditLogPath(_repoRoot) {
|
|
1847
|
-
return path9.join(".codex", "belay", "audit.ndjson");
|
|
1848
|
-
},
|
|
1849
|
-
repoRootMarkers: [".git", ".codex"],
|
|
1850
|
-
runnerCommand: runnerCommand2,
|
|
1851
|
-
defaultConfig(repoRoot) {
|
|
1852
|
-
return {
|
|
1853
|
-
...DEFAULT_CONFIG_V4,
|
|
1854
|
-
adapter: "codex",
|
|
1855
|
-
audit: {
|
|
1856
|
-
...DEFAULT_CONFIG_V4.audit,
|
|
1857
|
-
logPath: codexLayout.defaultAuditLogPath(repoRoot)
|
|
1858
|
-
}
|
|
1859
|
-
};
|
|
1860
|
-
}
|
|
1861
|
-
};
|
|
1862
|
-
|
|
1863
|
-
// src/adapters/layouts/cursor.ts
|
|
1864
|
-
import path10 from "node:path";
|
|
1865
|
-
function runnerCommand3(platform, repoRoot, hookName, ...args) {
|
|
1866
|
-
const hooksDir = path10.join(path10.resolve(repoRoot), ".cursor", "hooks");
|
|
1867
|
-
return buildRunnerInvocation(platform, hooksDir, repoRoot, hookName, ...args);
|
|
1868
|
-
}
|
|
1869
|
-
var cursorLayout = {
|
|
1870
|
-
name: "cursor",
|
|
1871
|
-
configPath(repoRoot) {
|
|
1872
|
-
return path10.join(repoRoot, ".cursor", "belay.config.json");
|
|
1873
|
-
},
|
|
1874
|
-
hooksSettingsPath(repoRoot) {
|
|
1875
|
-
return path10.join(repoRoot, ".cursor", "hooks.json");
|
|
1876
|
-
},
|
|
1877
|
-
hooksDir(repoRoot) {
|
|
1878
|
-
return path10.join(repoRoot, ".cursor", "hooks");
|
|
1879
|
-
},
|
|
1880
|
-
runtimeDir(repoRoot) {
|
|
1881
|
-
return path10.join(repoRoot, ".cursor", "belay", "runtime");
|
|
1882
|
-
},
|
|
1883
|
-
repoLocalStateDir(repoRoot) {
|
|
1884
|
-
return path10.join(repoRoot, ".cursor", "belay");
|
|
1885
|
-
},
|
|
1886
|
-
defaultAuditLogPath(_repoRoot) {
|
|
1887
|
-
return path10.join(".cursor", "belay", "audit.ndjson");
|
|
1888
|
-
},
|
|
1889
|
-
repoRootMarkers: [".git", ".cursor"],
|
|
1890
|
-
runnerCommand: runnerCommand3,
|
|
1891
|
-
defaultConfig(repoRoot) {
|
|
1892
|
-
return {
|
|
1893
|
-
...DEFAULT_CONFIG_V4,
|
|
1894
|
-
adapter: "cursor",
|
|
1895
|
-
audit: {
|
|
1896
|
-
...DEFAULT_CONFIG_V4.audit,
|
|
1897
|
-
logPath: cursorLayout.defaultAuditLogPath(repoRoot)
|
|
1898
|
-
}
|
|
1899
|
-
};
|
|
1900
|
-
}
|
|
1901
|
-
};
|
|
1902
|
-
|
|
1903
|
-
// src/adapters/layouts/index.ts
|
|
1904
|
-
var layouts = {
|
|
1905
|
-
cursor: cursorLayout,
|
|
1906
|
-
claude: claudeLayout,
|
|
1907
|
-
codex: codexLayout
|
|
1908
|
-
};
|
|
1909
|
-
function getAdapterLayout(name = "cursor") {
|
|
1910
|
-
return layouts[name];
|
|
1911
|
-
}
|
|
1912
|
-
|
|
1913
|
-
// src/config-io.ts
|
|
1914
|
-
function resolveAdapterName(config) {
|
|
1915
|
-
if (config.adapter === "claude") {
|
|
1916
|
-
return "claude";
|
|
2601
|
+
// src/core/verdict/judge-baseline.ts
|
|
2602
|
+
var samples = [];
|
|
2603
|
+
var MAX_SAMPLES = 2e3;
|
|
2604
|
+
function recordJudgeLatency(phase, latencyMs, at = Date.now()) {
|
|
2605
|
+
if (!Number.isFinite(latencyMs) || latencyMs < 0) {
|
|
2606
|
+
return;
|
|
1917
2607
|
}
|
|
1918
|
-
|
|
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,20 +8503,39 @@ async function evaluateGatedAction(ctx, deps, params) {
|
|
|
6233
8503
|
};
|
|
6234
8504
|
}
|
|
6235
8505
|
}
|
|
8506
|
+
const scrubOpts = scrubOptionsFromConfig(ctx.config);
|
|
8507
|
+
const scrubbedPayload = fingerprintReplayPayload(params.kind, params.payload, scrubOpts);
|
|
6236
8508
|
return gateDecisionToVerdict(ctx, deps, params.kind, result, {
|
|
6237
8509
|
predictedAssessment,
|
|
6238
8510
|
observedAssessment: observedAssessment2,
|
|
6239
8511
|
transactionalLayer,
|
|
6240
8512
|
approvalInput: params.kind === "shell" ? {
|
|
6241
8513
|
input: params.command ?? result.normalizedCommand ?? result.summary ?? "",
|
|
6242
|
-
inputKind: "shell"
|
|
8514
|
+
inputKind: "shell",
|
|
8515
|
+
cwd: params.cwd
|
|
6243
8516
|
} : {
|
|
6244
|
-
input: params.command ?? result.normalizedCommand ?? result.summary ?? canonicalStringify(
|
|
6245
|
-
inputKind: params.kind
|
|
6246
|
-
|
|
8517
|
+
input: params.command ?? result.normalizedCommand ?? result.summary ?? canonicalStringify(scrubbedPayload ?? {}),
|
|
8518
|
+
inputKind: params.kind,
|
|
8519
|
+
cwd: params.cwd,
|
|
8520
|
+
toolName: params.toolName,
|
|
8521
|
+
payload: scrubbedPayload
|
|
8522
|
+
},
|
|
8523
|
+
replayAction: {
|
|
8524
|
+
kind: params.kind,
|
|
8525
|
+
cwd: params.cwd,
|
|
8526
|
+
toolName: params.toolName,
|
|
8527
|
+
command: params.command,
|
|
8528
|
+
payload: scrubbedPayload,
|
|
8529
|
+
fingerprint: result.fingerprint,
|
|
8530
|
+
repoRoot: ctx.repoRoot
|
|
8531
|
+
},
|
|
8532
|
+
classifierOptions,
|
|
8533
|
+
scopeHintPayload: params.payload
|
|
6247
8534
|
});
|
|
6248
8535
|
}
|
|
6249
8536
|
async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {}) {
|
|
8537
|
+
const replayContext = buildAuditReplayContext(kind, result, auditExtras.replayAction);
|
|
8538
|
+
const actionSnapshot = buildAuditActionSnapshot(kind, result, auditExtras.replayAction);
|
|
6250
8539
|
const gateBase = {
|
|
6251
8540
|
event: gateAuditEventName(kind),
|
|
6252
8541
|
kind,
|
|
@@ -6258,7 +8547,9 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
6258
8547
|
mode: ctx.config.mode,
|
|
6259
8548
|
schemaVersion: result.axes ? 2 : 1,
|
|
6260
8549
|
...result.axes ?? {},
|
|
6261
|
-
...auditExtras.transactionalLayer
|
|
8550
|
+
...auditExtras.transactionalLayer,
|
|
8551
|
+
...replayContext ? { replayContext } : {},
|
|
8552
|
+
...actionSnapshot ? { actionSnapshot } : {}
|
|
6262
8553
|
};
|
|
6263
8554
|
if (result.reason === TRANSACTIONAL_ALREADY_APPLIED) {
|
|
6264
8555
|
const userMessage = "Belay executed this command safely in an isolated git worktree. Observed-safe file changes are already applied; do not retry the same command.";
|
|
@@ -6279,6 +8570,36 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
6279
8570
|
agent_message: agentMessage
|
|
6280
8571
|
});
|
|
6281
8572
|
}
|
|
8573
|
+
if (result.verdict === "deny_pending_approval" && ctx.config.mode === "enforce" && !TRANSACTIONAL_APPROVAL_BYPASS_REASONS.has(result.reason)) {
|
|
8574
|
+
const standingAllowPath = standingAllowFile(
|
|
8575
|
+
ctx.config,
|
|
8576
|
+
ctx.layout.repoLocalStateDir(ctx.repoRoot)
|
|
8577
|
+
);
|
|
8578
|
+
const standingState = await loadStandingAllow(standingAllowPath);
|
|
8579
|
+
const standingMatch = resolveStandingAllowMatch({
|
|
8580
|
+
kind,
|
|
8581
|
+
result,
|
|
8582
|
+
repoRoot: ctx.repoRoot,
|
|
8583
|
+
state: standingState
|
|
8584
|
+
});
|
|
8585
|
+
if (standingMatch) {
|
|
8586
|
+
await deps.appendAudit(ctx, {
|
|
8587
|
+
...gateBase,
|
|
8588
|
+
verdict: "allow",
|
|
8589
|
+
reason: "standing_allow",
|
|
8590
|
+
standingAllowSource: standingMatch.source,
|
|
8591
|
+
...standingMatch.catalogCommand ? { standingAllowCatalogCommand: standingMatch.catalogCommand } : {},
|
|
8592
|
+
wouldBlock: false,
|
|
8593
|
+
permission: "allow"
|
|
8594
|
+
});
|
|
8595
|
+
return classifyResultToGateVerdict({
|
|
8596
|
+
result: { ...result, verdict: "allow", reason: "standing_allow" },
|
|
8597
|
+
mode: ctx.config.mode,
|
|
8598
|
+
permission: "allow",
|
|
8599
|
+
wouldBlock: false
|
|
8600
|
+
});
|
|
8601
|
+
}
|
|
8602
|
+
}
|
|
6282
8603
|
const brokerActive = isCapabilityBrokerDemotionActive(ctx.config);
|
|
6283
8604
|
let approved = null;
|
|
6284
8605
|
if (!TRANSACTIONAL_APPROVAL_BYPASS_REASONS.has(result.reason) && !shouldSkipBrokerApprovedOnce(brokerActive, result.reason)) {
|
|
@@ -6288,6 +8609,29 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
6288
8609
|
(entry) => entry.kind === kind && entry.fingerprint === result.fingerprint && entry.repoRoot === ctx.repoRoot
|
|
6289
8610
|
);
|
|
6290
8611
|
if (!shouldSkipBrokerApprovedRecord(brokerActive, matchedApproval?.reason)) {
|
|
8612
|
+
if (matchedApproval && auditExtras.replayAction && !validateReplayEnvelope(matchedApproval, auditExtras.replayAction)) {
|
|
8613
|
+
await deps.appendAudit(ctx, {
|
|
8614
|
+
...gateBase,
|
|
8615
|
+
verdict: "deny_pending_approval",
|
|
8616
|
+
reason: "approval_replay_mismatch",
|
|
8617
|
+
approvalId: matchedApproval.approvalId,
|
|
8618
|
+
wouldBlock: true,
|
|
8619
|
+
permission: "deny"
|
|
8620
|
+
});
|
|
8621
|
+
return classifyResultToGateVerdict({
|
|
8622
|
+
result: {
|
|
8623
|
+
...result,
|
|
8624
|
+
verdict: "deny_pending_approval",
|
|
8625
|
+
reason: "approval_replay_mismatch"
|
|
8626
|
+
},
|
|
8627
|
+
mode: ctx.config.mode,
|
|
8628
|
+
permission: "deny",
|
|
8629
|
+
wouldBlock: true,
|
|
8630
|
+
approvalId: matchedApproval.approvalId,
|
|
8631
|
+
user_message: "Belay denied this action because it does not match the approved replay envelope. Re-approve the exact action or run belay explain.",
|
|
8632
|
+
agent_message: "Belay denied this action because cwd, tool, or payload changed after approval."
|
|
8633
|
+
});
|
|
8634
|
+
}
|
|
6291
8635
|
approved = await consumeApprovedApproval(ctx, deps, kind, result.fingerprint);
|
|
6292
8636
|
}
|
|
6293
8637
|
}
|
|
@@ -6338,7 +8682,19 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
6338
8682
|
wouldBlock: true
|
|
6339
8683
|
});
|
|
6340
8684
|
}
|
|
6341
|
-
const approval = await ensurePendingApproval(
|
|
8685
|
+
const approval = await ensurePendingApproval(
|
|
8686
|
+
ctx,
|
|
8687
|
+
deps,
|
|
8688
|
+
kind,
|
|
8689
|
+
result,
|
|
8690
|
+
auditExtras.approvalInput,
|
|
8691
|
+
deriveWorkspaceRootScopeHint({
|
|
8692
|
+
result,
|
|
8693
|
+
replayAction: auditExtras.replayAction,
|
|
8694
|
+
payloadForScopeHint: auditExtras.scopeHintPayload,
|
|
8695
|
+
options: auditExtras.classifierOptions ?? {}
|
|
8696
|
+
})
|
|
8697
|
+
);
|
|
6342
8698
|
let approvalToken;
|
|
6343
8699
|
try {
|
|
6344
8700
|
approvalToken = await issueApprovalToken(
|
|
@@ -6378,7 +8734,7 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
6378
8734
|
permission: "deny",
|
|
6379
8735
|
wouldBlock: true,
|
|
6380
8736
|
approvalId: approval.approvalId,
|
|
6381
|
-
user_message: `Belay blocked this high-risk action. Approval ID: ${approval.approvalId}. ${
|
|
8737
|
+
user_message: `Belay blocked this high-risk action. Approval ID: ${approval.approvalId}. ${buildRetryInstructionForConfig(ctx.config, ctx.config.tokenPrefix, approval.approvalId)} For details, run belay explain or /belay why.`,
|
|
6382
8738
|
agent_message: `Belay denied this action as ${result.reason}. Wait for approval, then retry the exact same action once.`
|
|
6383
8739
|
});
|
|
6384
8740
|
}
|
|
@@ -6387,6 +8743,7 @@ async function processApprovalPrompt(ctx, deps, prompt) {
|
|
|
6387
8743
|
if (!approvalId) {
|
|
6388
8744
|
return { continue: true };
|
|
6389
8745
|
}
|
|
8746
|
+
const adapter = adapterIdFromContext(ctx);
|
|
6390
8747
|
if (ctx.config.approvalSigning.required) {
|
|
6391
8748
|
const message = `Signed approval token required for ${approvalId}. Editor prompt approval is disabled in this configuration. Use belay approve --approval-id ${approvalId} --token <signed-token>.`;
|
|
6392
8749
|
await deps.appendAudit(ctx, {
|
|
@@ -6406,12 +8763,11 @@ async function processApprovalPrompt(ctx, deps, prompt) {
|
|
|
6406
8763
|
approvalId,
|
|
6407
8764
|
config: ctx.config,
|
|
6408
8765
|
requireSignedToken: ctx.config.approvalSigning.required,
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
}
|
|
8766
|
+
adapter,
|
|
8767
|
+
store: gateApprovalStoreFromDeps({
|
|
8768
|
+
loadApprovals: (fileName) => deps.loadApprovals(ctx, fileName),
|
|
8769
|
+
writeApprovals: (filePath, state) => deps.writeApprovals(filePath, state)
|
|
8770
|
+
})
|
|
6415
8771
|
});
|
|
6416
8772
|
await deps.appendAudit(ctx, {
|
|
6417
8773
|
event: "approval",
|
|
@@ -6427,9 +8783,91 @@ async function processApprovalPrompt(ctx, deps, prompt) {
|
|
|
6427
8783
|
user_message: recorded.message
|
|
6428
8784
|
};
|
|
6429
8785
|
}
|
|
8786
|
+
const replay = recorded.approval ? buildReplayHint(ctx.config, recorded.approval, adapter) : null;
|
|
8787
|
+
const shouldFallbackReplay = adapter === "cursor" && replay?.kind === "shell" && replay.autoReplay === true && Boolean(recorded.approval?.input) && (!process.env.VITEST || process.env.BELAY_TEST_APPROVAL_REPLAY === "1");
|
|
8788
|
+
if (shouldFallbackReplay && recorded.approval) {
|
|
8789
|
+
const approvalStore = gateApprovalStoreFromDeps({
|
|
8790
|
+
loadApprovals: (fileName) => deps.loadApprovals(ctx, fileName),
|
|
8791
|
+
writeApprovals: (filePath, state) => deps.writeApprovals(filePath, state)
|
|
8792
|
+
});
|
|
8793
|
+
let replayResult;
|
|
8794
|
+
try {
|
|
8795
|
+
replayResult = await replayShellCommand(
|
|
8796
|
+
recorded.approval.input ?? "",
|
|
8797
|
+
recorded.approval.cwd ?? ctx.repoRoot,
|
|
8798
|
+
getExecutionLeaseMs(ctx.config)
|
|
8799
|
+
);
|
|
8800
|
+
} catch (error) {
|
|
8801
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
8802
|
+
await deps.appendAudit(ctx, {
|
|
8803
|
+
event: "approval",
|
|
8804
|
+
kind: "approval",
|
|
8805
|
+
verdict: "allow",
|
|
8806
|
+
approvalId,
|
|
8807
|
+
reason: "approval_replay_error",
|
|
8808
|
+
summary: recorded.approval.input ?? recorded.approval.summary ?? prompt
|
|
8809
|
+
});
|
|
8810
|
+
return {
|
|
8811
|
+
continue: false,
|
|
8812
|
+
user_message: `Belay approval recorded for ${approvalId}, but one-step shell replay could not start (${detail}). Approval remains active for one hook retry.`
|
|
8813
|
+
};
|
|
8814
|
+
}
|
|
8815
|
+
const output = [replayResult.stdout, replayResult.stderr].filter(Boolean).join("\n").trim();
|
|
8816
|
+
if (replayResult.exitCode === 0) {
|
|
8817
|
+
try {
|
|
8818
|
+
await consumeApprovedAfterCliReplay({
|
|
8819
|
+
approvalId,
|
|
8820
|
+
store: approvalStore
|
|
8821
|
+
});
|
|
8822
|
+
} catch (error) {
|
|
8823
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
8824
|
+
await deps.appendAudit(ctx, {
|
|
8825
|
+
event: "approval",
|
|
8826
|
+
kind: "approval",
|
|
8827
|
+
verdict: "allow",
|
|
8828
|
+
approvalId,
|
|
8829
|
+
reason: "approval_replay_consume_error",
|
|
8830
|
+
summary: recorded.approval.input ?? recorded.approval.summary ?? prompt
|
|
8831
|
+
});
|
|
8832
|
+
return {
|
|
8833
|
+
continue: false,
|
|
8834
|
+
user_message: `Belay approval recorded for ${approvalId}, and one-step shell replay succeeded, but approval finalization failed (${detail}). Approval remains active for one hook retry.` + (output ? `
|
|
8835
|
+
${output}` : "")
|
|
8836
|
+
};
|
|
8837
|
+
}
|
|
8838
|
+
await deps.appendAudit(ctx, {
|
|
8839
|
+
event: "approval",
|
|
8840
|
+
kind: "approval",
|
|
8841
|
+
verdict: "allow",
|
|
8842
|
+
approvalId,
|
|
8843
|
+
reason: "approval_replay_succeeded",
|
|
8844
|
+
summary: recorded.approval.input ?? recorded.approval.summary ?? prompt
|
|
8845
|
+
});
|
|
8846
|
+
return {
|
|
8847
|
+
continue: false,
|
|
8848
|
+
user_message: `Belay approval recorded for ${approvalId}. One-step shell replay succeeded; no manual retry required.` + (output ? `
|
|
8849
|
+
${output}` : "")
|
|
8850
|
+
};
|
|
8851
|
+
}
|
|
8852
|
+
await deps.appendAudit(ctx, {
|
|
8853
|
+
event: "approval",
|
|
8854
|
+
kind: "approval",
|
|
8855
|
+
verdict: "allow",
|
|
8856
|
+
approvalId,
|
|
8857
|
+
reason: "approval_replay_failed",
|
|
8858
|
+
summary: recorded.approval.input ?? recorded.approval.summary ?? prompt
|
|
8859
|
+
});
|
|
8860
|
+
const timeoutNote = replayResult.timedOut ? " Replay timed out." : "";
|
|
8861
|
+
return {
|
|
8862
|
+
continue: false,
|
|
8863
|
+
user_message: `Belay approval recorded for ${approvalId}, but one-step shell replay failed (exit ${replayResult.exitCode}).${timeoutNote} Approval remains active for one hook retry.` + (output ? `
|
|
8864
|
+
${output}` : "")
|
|
8865
|
+
};
|
|
8866
|
+
}
|
|
6430
8867
|
return {
|
|
6431
8868
|
continue: false,
|
|
6432
|
-
user_message: recorded.message
|
|
8869
|
+
user_message: recorded.message,
|
|
8870
|
+
...replay ? { replay } : {}
|
|
6433
8871
|
};
|
|
6434
8872
|
}
|
|
6435
8873
|
function gateVerdictToClaudePreToolUseResponse(verdict2) {
|
|
@@ -6452,7 +8890,8 @@ function gateVerdictToClaudeUserPromptResponse(verdict2) {
|
|
|
6452
8890
|
hookSpecificOutput: {
|
|
6453
8891
|
hookEventName: "UserPromptSubmit",
|
|
6454
8892
|
continue: false,
|
|
6455
|
-
user_message: verdict2.user_message
|
|
8893
|
+
user_message: verdict2.user_message,
|
|
8894
|
+
...verdict2.replay ? { replay: verdict2.replay } : {}
|
|
6456
8895
|
}
|
|
6457
8896
|
};
|
|
6458
8897
|
}
|
|
@@ -6467,19 +8906,19 @@ async function appendObservedAudit(ctx, deps, eventName, payload) {
|
|
|
6467
8906
|
}
|
|
6468
8907
|
|
|
6469
8908
|
// src/adapters/shared/repo-root.ts
|
|
6470
|
-
import { existsSync as
|
|
6471
|
-
import
|
|
8909
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
8910
|
+
import path27 from "node:path";
|
|
6472
8911
|
function findRepoRoot(startPath, layout) {
|
|
6473
|
-
let current =
|
|
8912
|
+
let current = path27.resolve(startPath);
|
|
6474
8913
|
while (true) {
|
|
6475
8914
|
for (const marker of layout.repoRootMarkers) {
|
|
6476
|
-
if (
|
|
8915
|
+
if (existsSync11(path27.join(current, marker))) {
|
|
6477
8916
|
return current;
|
|
6478
8917
|
}
|
|
6479
8918
|
}
|
|
6480
|
-
const parent =
|
|
8919
|
+
const parent = path27.dirname(current);
|
|
6481
8920
|
if (parent === current) {
|
|
6482
|
-
return
|
|
8921
|
+
return path27.resolve(startPath);
|
|
6483
8922
|
}
|
|
6484
8923
|
current = parent;
|
|
6485
8924
|
}
|