@guilz-dev/belay 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -12
- package/dist/adapters/shared/gate-runtime.js +13 -5
- package/dist/bundle/claude-runtime.mjs +1659 -371
- package/dist/bundle/codex-runtime.mjs +1658 -371
- package/dist/bundle/cursor-runtime.mjs +1659 -371
- package/dist/cli.js +198 -8
- package/dist/commands/approve.js +11 -0
- package/dist/commands/config.d.ts +70 -0
- package/dist/commands/config.js +494 -0
- package/dist/commands/doctor.js +2 -1
- package/dist/commands/health-snapshot.d.ts +6 -0
- package/dist/commands/health-snapshot.js +34 -0
- package/dist/commands/judge.d.ts +90 -0
- package/dist/commands/judge.js +285 -0
- package/dist/commands/status.js +2 -2
- package/dist/commands/stdin-key.d.ts +1 -0
- package/dist/commands/stdin-key.js +8 -0
- package/dist/conformance/guarantee-table.js +12 -0
- package/dist/conformance/types.d.ts +2 -0
- package/dist/core/audit-io.d.ts +2 -0
- package/dist/core/audit-io.js +14 -0
- package/dist/core/capability/index.d.ts +2 -2
- package/dist/core/capability/index.js +2 -2
- package/dist/core/capability/paths.d.ts +1 -0
- package/dist/core/capability/paths.js +74 -6
- package/dist/core/capability/reasons.d.ts +3 -0
- package/dist/core/capability/reasons.js +8 -0
- package/dist/core/classify-tool.js +97 -27
- package/dist/core/config-layers.js +2 -1
- package/dist/core/config.d.ts +22 -2
- package/dist/core/config.js +110 -11
- package/dist/core/credential-store.d.ts +11 -0
- package/dist/core/credential-store.js +60 -0
- package/dist/core/gate-engine.js +104 -13
- package/dist/core/integrity.d.ts +2 -0
- package/dist/core/integrity.js +13 -0
- package/dist/core/judge-api-key.d.ts +19 -0
- package/dist/core/judge-api-key.js +74 -0
- package/dist/core/judge-cloud-consent.d.ts +13 -0
- package/dist/core/judge-cloud-consent.js +38 -0
- package/dist/core/judge-config.d.ts +41 -4
- package/dist/core/judge-config.js +263 -57
- package/dist/core/judge-doctor.d.ts +6 -1
- package/dist/core/judge-doctor.js +147 -96
- package/dist/core/judge-model-discovery.d.ts +24 -0
- package/dist/core/judge-model-discovery.js +168 -0
- package/dist/core/judge-model-policy.d.ts +5 -0
- package/dist/core/judge-model-policy.js +21 -0
- package/dist/core/judge-runtime-detection.d.ts +9 -0
- package/dist/core/judge-runtime-detection.js +68 -0
- package/dist/core/transactional/diff-evaluator.js +1 -19
- package/dist/core/types.d.ts +2 -0
- package/dist/core/verdict/adapter.d.ts +1 -0
- package/dist/core/verdict/adapter.js +7 -1
- package/dist/core/verdict/containment.d.ts +5 -0
- package/dist/core/verdict/containment.js +32 -2
- package/dist/core/verdict/judge-catalog.d.ts +40 -0
- package/dist/core/verdict/judge-catalog.js +148 -0
- package/dist/core/verdict/judge-cli.d.ts +23 -0
- package/dist/core/verdict/judge-cli.js +280 -0
- package/dist/core/verdict/judge-factory.d.ts +15 -4
- package/dist/core/verdict/judge-factory.js +117 -14
- package/dist/core/verdict/judge.d.ts +20 -1
- package/dist/core/verdict/judge.js +83 -14
- package/dist/core/verdict/persistent-paths.d.ts +8 -0
- package/dist/core/verdict/persistent-paths.js +52 -0
- package/dist/core/verdict/types.d.ts +6 -2
- package/dist/core/verdict/verdict.js +161 -47
- package/dist/installer.js +66 -15
- package/dist/types.d.ts +7 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/belay/SKILL.md +8 -7
- package/dist/commands/init-wizard.d.ts +0 -21
- package/dist/commands/init-wizard.js +0 -63
|
@@ -1,35 +1,4 @@
|
|
|
1
1
|
// agent-belay codex runtime bundle
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
-
var __esm = (fn, res) => function __init() {
|
|
5
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
-
};
|
|
7
|
-
var __export = (target, all) => {
|
|
8
|
-
for (var name in all)
|
|
9
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
// src/core/judge-api-key.ts
|
|
13
|
-
var judge_api_key_exports = {};
|
|
14
|
-
__export(judge_api_key_exports, {
|
|
15
|
-
resolveJudgeApiKey: () => resolveJudgeApiKey
|
|
16
|
-
});
|
|
17
|
-
function resolveJudgeApiKey(env = process.env) {
|
|
18
|
-
const belay = env.BELAY_JUDGE_API_KEY?.trim();
|
|
19
|
-
if (belay) {
|
|
20
|
-
return { key: belay, source: "BELAY_JUDGE_API_KEY" };
|
|
21
|
-
}
|
|
22
|
-
const openai = env.OPENAI_API_KEY?.trim();
|
|
23
|
-
if (openai) {
|
|
24
|
-
return { key: openai, source: "OPENAI_API_KEY" };
|
|
25
|
-
}
|
|
26
|
-
return { key: null, source: null };
|
|
27
|
-
}
|
|
28
|
-
var init_judge_api_key = __esm({
|
|
29
|
-
"src/core/judge-api-key.ts"() {
|
|
30
|
-
"use strict";
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
2
|
|
|
34
3
|
// src/adapters/codex/runtime-entry.ts
|
|
35
4
|
import process2 from "node:process";
|
|
@@ -40,12 +9,158 @@ import path3 from "node:path";
|
|
|
40
9
|
// src/core/config.ts
|
|
41
10
|
import path from "node:path";
|
|
42
11
|
|
|
12
|
+
// src/core/judge-model-policy.ts
|
|
13
|
+
var DEPRECATED_JUDGE_MODEL_AUTO = "auto";
|
|
14
|
+
function isDeprecatedJudgeModelAuto(model) {
|
|
15
|
+
return model?.trim().toLowerCase() === DEPRECATED_JUDGE_MODEL_AUTO;
|
|
16
|
+
}
|
|
17
|
+
function rejectDeprecatedJudgeModelAuto(model) {
|
|
18
|
+
if (isDeprecatedJudgeModelAuto(model)) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
'judge model "auto" is no longer accepted. Use a concrete model id from belay judge list or the provider catalog default.'
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
var warnedDeprecatedAuto = false;
|
|
25
|
+
function warnDeprecatedJudgeModelAuto() {
|
|
26
|
+
if (warnedDeprecatedAuto) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
warnedDeprecatedAuto = true;
|
|
30
|
+
process.stderr.write(
|
|
31
|
+
'Warning: judge.model "auto" is deprecated; normalized to the provider catalog default on load. Set a concrete model id with belay config set judge.model <id>.\n'
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/core/verdict/judge-catalog.ts
|
|
36
|
+
var LEGACY_PROVIDER_ID_ALIASES = {
|
|
37
|
+
local: "ollama",
|
|
38
|
+
openai: "codex"
|
|
39
|
+
};
|
|
40
|
+
var REMOVED_JUDGE_PROVIDER_IDS = ["openrouter", "custom"];
|
|
41
|
+
function isRemovedProviderId(id) {
|
|
42
|
+
return REMOVED_JUDGE_PROVIDER_IDS.includes(id);
|
|
43
|
+
}
|
|
44
|
+
function warnRemovedProviderId(id) {
|
|
45
|
+
process.stderr.write(
|
|
46
|
+
`Warning: judge.providerId "${id}" was removed; migrate with belay config set judge.providerId <ollama|codex|claude|cursor>.
|
|
47
|
+
`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
var JUDGE_CATALOG = {
|
|
51
|
+
ollama: {
|
|
52
|
+
id: "ollama",
|
|
53
|
+
driver: "ollama",
|
|
54
|
+
defaultEndpoint: "http://localhost:11434",
|
|
55
|
+
defaultModel: "gemma4:e2b",
|
|
56
|
+
apiKeyEnvVars: [],
|
|
57
|
+
isCloud: false
|
|
58
|
+
},
|
|
59
|
+
codex: {
|
|
60
|
+
id: "codex",
|
|
61
|
+
driver: "openai-compatible",
|
|
62
|
+
defaultEndpoint: null,
|
|
63
|
+
defaultModel: "gpt-5.3-codex-high",
|
|
64
|
+
apiKeyEnvVars: ["OPENAI_API_KEY", "BELAY_JUDGE_API_KEY"],
|
|
65
|
+
isCloud: true
|
|
66
|
+
},
|
|
67
|
+
claude: {
|
|
68
|
+
id: "claude",
|
|
69
|
+
driver: "anthropic",
|
|
70
|
+
defaultEndpoint: null,
|
|
71
|
+
defaultModel: "claude-sonnet-4-6",
|
|
72
|
+
apiKeyEnvVars: ["ANTHROPIC_API_KEY", "BELAY_JUDGE_API_KEY"],
|
|
73
|
+
isCloud: true
|
|
74
|
+
},
|
|
75
|
+
cursor: {
|
|
76
|
+
id: "cursor",
|
|
77
|
+
driver: "openai-compatible",
|
|
78
|
+
defaultEndpoint: null,
|
|
79
|
+
defaultModel: "composer-2.5",
|
|
80
|
+
apiKeyEnvVars: ["CURSOR_API_KEY", "BELAY_JUDGE_API_KEY"],
|
|
81
|
+
isCloud: true
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
var JUDGE_PROVIDER_IDS = Object.keys(JUDGE_CATALOG);
|
|
85
|
+
function normalizeLegacyProviderId(id) {
|
|
86
|
+
if (id in JUDGE_CATALOG) {
|
|
87
|
+
return id;
|
|
88
|
+
}
|
|
89
|
+
return LEGACY_PROVIDER_ID_ALIASES[id] ?? null;
|
|
90
|
+
}
|
|
91
|
+
function getJudgeProviderSpec(id) {
|
|
92
|
+
const normalized = normalizeLegacyProviderId(id);
|
|
93
|
+
if (!normalized) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return JUDGE_CATALOG[normalized];
|
|
97
|
+
}
|
|
98
|
+
function inferProviderIdFromConfig(judge) {
|
|
99
|
+
if (judge.providerId) {
|
|
100
|
+
const normalized = normalizeLegacyProviderId(judge.providerId);
|
|
101
|
+
if (normalized) {
|
|
102
|
+
return normalized;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (judge.provider === "ollama") {
|
|
106
|
+
return "ollama";
|
|
107
|
+
}
|
|
108
|
+
if (judge.provider === "anthropic") {
|
|
109
|
+
return "claude";
|
|
110
|
+
}
|
|
111
|
+
const endpoint = judge.endpoint?.trim().toLowerCase() ?? "";
|
|
112
|
+
if (endpoint.includes("openrouter.ai")) {
|
|
113
|
+
return "codex";
|
|
114
|
+
}
|
|
115
|
+
if (endpoint.includes("api.openai.com")) {
|
|
116
|
+
return "codex";
|
|
117
|
+
}
|
|
118
|
+
if (endpoint) {
|
|
119
|
+
return "codex";
|
|
120
|
+
}
|
|
121
|
+
if (judge.provider === "openai-compatible") {
|
|
122
|
+
const model = judge.model?.trim().toLowerCase() ?? "";
|
|
123
|
+
if (model.includes("composer")) {
|
|
124
|
+
return "cursor";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return "codex";
|
|
128
|
+
}
|
|
129
|
+
function resolveJudgeFromCatalog(input) {
|
|
130
|
+
const providerId = normalizeLegacyProviderId(String(input.providerId));
|
|
131
|
+
if (!providerId) {
|
|
132
|
+
throw new Error(`Unknown judge provider id: ${input.providerId}`);
|
|
133
|
+
}
|
|
134
|
+
const spec = JUDGE_CATALOG[providerId];
|
|
135
|
+
const model = input.model?.trim() || spec.defaultModel;
|
|
136
|
+
const endpoint = input.endpoint !== void 0 ? input.endpoint?.trim() || null : spec.defaultEndpoint;
|
|
137
|
+
return {
|
|
138
|
+
provider: spec.driver,
|
|
139
|
+
providerId: spec.id,
|
|
140
|
+
model,
|
|
141
|
+
endpoint,
|
|
142
|
+
timeoutMs: input.timeoutMs ?? (spec.isCloud ? 8e3 : 25e3),
|
|
143
|
+
keepAlive: spec.driver === "ollama" ? input.keepAlive ?? "30m" : null
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function isCloudProviderId(providerId) {
|
|
147
|
+
if (!providerId) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
const normalized = normalizeLegacyProviderId(providerId);
|
|
151
|
+
if (!normalized) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
return JUDGE_CATALOG[normalized].isCloud;
|
|
155
|
+
}
|
|
156
|
+
|
|
43
157
|
// src/core/audit-summary.ts
|
|
44
158
|
var DEFAULT_SILENT_PASS_THRESHOLD = 0.5;
|
|
45
159
|
|
|
46
160
|
// src/core/config.ts
|
|
47
161
|
var DEFAULT_JUDGE_LOCAL_OLLAMA = {
|
|
48
162
|
provider: "ollama",
|
|
163
|
+
providerId: "ollama",
|
|
49
164
|
model: "gemma4:e2b",
|
|
50
165
|
endpoint: "http://localhost:11434",
|
|
51
166
|
timeoutMs: 25e3,
|
|
@@ -53,7 +168,8 @@ var DEFAULT_JUDGE_LOCAL_OLLAMA = {
|
|
|
53
168
|
};
|
|
54
169
|
var DEFAULT_JUDGE_OPENAI_COMPATIBLE_TEMPLATE = {
|
|
55
170
|
provider: "openai-compatible",
|
|
56
|
-
|
|
171
|
+
providerId: "codex",
|
|
172
|
+
model: "gpt-5.3-codex-high",
|
|
57
173
|
timeoutMs: 8e3,
|
|
58
174
|
endpoint: null,
|
|
59
175
|
keepAlive: null
|
|
@@ -229,36 +345,125 @@ function migrateV2ToV3(v2, rawOverrides) {
|
|
|
229
345
|
});
|
|
230
346
|
}
|
|
231
347
|
function normalizeJudgeProvider(provider) {
|
|
348
|
+
if (provider === "anthropic") {
|
|
349
|
+
return "anthropic";
|
|
350
|
+
}
|
|
232
351
|
if (provider === "openai-compatible" || provider === "cursor") {
|
|
233
352
|
return "openai-compatible";
|
|
234
353
|
}
|
|
235
354
|
return "ollama";
|
|
236
355
|
}
|
|
356
|
+
function defaultJudgeTemplateForProvider(provider) {
|
|
357
|
+
if (provider === "ollama") {
|
|
358
|
+
return DEFAULT_JUDGE_LOCAL_OLLAMA;
|
|
359
|
+
}
|
|
360
|
+
if (provider === "anthropic") {
|
|
361
|
+
return {
|
|
362
|
+
provider: "anthropic",
|
|
363
|
+
providerId: "claude",
|
|
364
|
+
model: "claude-sonnet-4-6",
|
|
365
|
+
timeoutMs: 8e3,
|
|
366
|
+
endpoint: null,
|
|
367
|
+
keepAlive: null
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
return DEFAULT_JUDGE_OPENAI_COMPATIBLE_TEMPLATE;
|
|
371
|
+
}
|
|
237
372
|
function synthesizeJudgeFromRaw(raw) {
|
|
238
373
|
const judge = raw.judge;
|
|
239
374
|
if (judge?.provider) {
|
|
240
|
-
const
|
|
241
|
-
const
|
|
375
|
+
const rawProvider = String(judge.provider);
|
|
376
|
+
const provider = normalizeJudgeProvider(rawProvider);
|
|
377
|
+
const base = defaultJudgeTemplateForProvider(provider);
|
|
378
|
+
const providerId = judge.providerId && normalizeLegacyProviderId(judge.providerId) ? normalizeLegacyProviderId(judge.providerId) : rawProvider === "cursor" ? "cursor" : void 0;
|
|
242
379
|
return normalizeJudgeConfig({
|
|
243
380
|
...base,
|
|
244
381
|
...judge,
|
|
245
|
-
provider
|
|
382
|
+
provider,
|
|
383
|
+
...providerId ? { providerId } : {}
|
|
246
384
|
});
|
|
247
385
|
}
|
|
248
386
|
return { ...DEFAULT_JUDGE_LOCAL_OLLAMA };
|
|
249
387
|
}
|
|
250
388
|
function normalizeJudgeConfig(judge) {
|
|
251
389
|
const provider = normalizeJudgeProvider(judge.provider);
|
|
252
|
-
const base = provider
|
|
253
|
-
const
|
|
390
|
+
const base = defaultJudgeTemplateForProvider(provider);
|
|
391
|
+
const rawProviderId = judge.providerId ? String(judge.providerId) : void 0;
|
|
392
|
+
if (rawProviderId && isRemovedProviderId(rawProviderId)) {
|
|
393
|
+
warnRemovedProviderId(rawProviderId);
|
|
394
|
+
let model2 = typeof judge.model === "string" && judge.model.trim() ? judge.model.trim() : base.model;
|
|
395
|
+
if (model2 === "auto") {
|
|
396
|
+
warnDeprecatedJudgeModelAuto();
|
|
397
|
+
model2 = base.model;
|
|
398
|
+
}
|
|
399
|
+
const timeoutMs2 = typeof judge.timeoutMs === "number" && judge.timeoutMs > 0 ? judge.timeoutMs : base.timeoutMs;
|
|
400
|
+
const endpoint2 = typeof judge.endpoint === "string" && judge.endpoint.trim() ? judge.endpoint.trim() : null;
|
|
401
|
+
return {
|
|
402
|
+
provider,
|
|
403
|
+
providerId: rawProviderId,
|
|
404
|
+
model: model2,
|
|
405
|
+
timeoutMs: timeoutMs2,
|
|
406
|
+
endpoint: endpoint2,
|
|
407
|
+
keepAlive: provider === "ollama" && typeof judge.keepAlive === "string" && judge.keepAlive.trim() ? judge.keepAlive.trim() : provider === "ollama" ? DEFAULT_JUDGE_LOCAL_OLLAMA.keepAlive : null,
|
|
408
|
+
...judge.cloudConsent ? { cloudConsent: judge.cloudConsent } : {},
|
|
409
|
+
...judge.credential ? { credential: judge.credential } : {}
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
let providerId = rawProviderId && normalizeLegacyProviderId(rawProviderId) ? normalizeLegacyProviderId(rawProviderId) : inferProviderIdFromConfig({ ...judge, provider });
|
|
413
|
+
const spec = getJudgeProviderSpec(providerId);
|
|
414
|
+
if (spec && spec.driver !== provider) {
|
|
415
|
+
providerId = inferProviderIdFromConfig({ ...judge, provider });
|
|
416
|
+
}
|
|
417
|
+
const catalogSpec = getJudgeProviderSpec(providerId);
|
|
418
|
+
let model = typeof judge.model === "string" && judge.model.trim() ? judge.model.trim() : base.model;
|
|
419
|
+
if (model === "auto" && catalogSpec?.defaultModel) {
|
|
420
|
+
warnDeprecatedJudgeModelAuto();
|
|
421
|
+
model = catalogSpec.defaultModel;
|
|
422
|
+
}
|
|
423
|
+
if (!model && catalogSpec?.defaultModel) {
|
|
424
|
+
model = catalogSpec.defaultModel;
|
|
425
|
+
}
|
|
254
426
|
const timeoutMs = typeof judge.timeoutMs === "number" && judge.timeoutMs > 0 ? judge.timeoutMs : base.timeoutMs;
|
|
255
|
-
|
|
427
|
+
let endpoint = typeof judge.endpoint === "string" && judge.endpoint.trim() ? judge.endpoint.trim() : null;
|
|
428
|
+
if (!endpoint && catalogSpec?.defaultEndpoint) {
|
|
429
|
+
endpoint = catalogSpec.defaultEndpoint;
|
|
430
|
+
}
|
|
431
|
+
const normalized = {
|
|
256
432
|
provider,
|
|
433
|
+
providerId,
|
|
257
434
|
model,
|
|
258
435
|
timeoutMs,
|
|
259
|
-
endpoint
|
|
436
|
+
endpoint,
|
|
260
437
|
keepAlive: provider === "ollama" && typeof judge.keepAlive === "string" && judge.keepAlive.trim() ? judge.keepAlive.trim() : provider === "ollama" ? DEFAULT_JUDGE_LOCAL_OLLAMA.keepAlive : null
|
|
261
438
|
};
|
|
439
|
+
if (judge.cloudConsent?.accepted) {
|
|
440
|
+
normalized.cloudConsent = {
|
|
441
|
+
accepted: true,
|
|
442
|
+
at: judge.cloudConsent.at,
|
|
443
|
+
providerId: judge.cloudConsent.providerId ?? providerId,
|
|
444
|
+
endpoint: judge.cloudConsent.endpoint,
|
|
445
|
+
by: judge.cloudConsent.by
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
if (judge.credential?.mode === "project" || judge.credential?.mode === "apiKey") {
|
|
449
|
+
normalized.credential = {
|
|
450
|
+
mode: judge.credential.mode,
|
|
451
|
+
...judge.credential.ref ? { ref: judge.credential.ref } : {}
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
return normalized;
|
|
455
|
+
}
|
|
456
|
+
function rejectTeamLayerJudgeSecrets(judge, source) {
|
|
457
|
+
if (source !== "team" || !judge) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (judge.credential?.mode === "apiKey") {
|
|
461
|
+
throw new Error("team config cannot set judge.credential.mode to apiKey.");
|
|
462
|
+
}
|
|
463
|
+
const raw = judge;
|
|
464
|
+
if (raw.credential && "key" in raw.credential && raw.credential.key) {
|
|
465
|
+
throw new Error("team config cannot contain inline judge credential keys.");
|
|
466
|
+
}
|
|
262
467
|
}
|
|
263
468
|
function hasV3Sections(raw) {
|
|
264
469
|
return raw.policy !== void 0 || raw.overrides !== void 0 || raw.redaction !== void 0 || raw.controlPlane !== void 0;
|
|
@@ -700,8 +905,8 @@ var codexLayout = {
|
|
|
700
905
|
// src/adapters/shared/gate-runtime.ts
|
|
701
906
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
702
907
|
import { existsSync as existsSync6 } from "node:fs";
|
|
703
|
-
import { mkdir as
|
|
704
|
-
import
|
|
908
|
+
import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
909
|
+
import path20 from "node:path";
|
|
705
910
|
|
|
706
911
|
// src/core/approval.ts
|
|
707
912
|
var APPROVAL_EXECUTION_LEASE_MS = 6e4;
|
|
@@ -972,6 +1177,9 @@ function isCapabilityBrokerDemotionActive(config) {
|
|
|
972
1177
|
return hasSandboxRuntime(config);
|
|
973
1178
|
}
|
|
974
1179
|
|
|
1180
|
+
// src/core/capability/paths.ts
|
|
1181
|
+
import path7 from "node:path";
|
|
1182
|
+
|
|
975
1183
|
// src/core/shell-tokenizer.ts
|
|
976
1184
|
function tokenizeShell(input) {
|
|
977
1185
|
const tokens = [];
|
|
@@ -1069,33 +1277,106 @@ function extractRedirectTargets(tokens) {
|
|
|
1069
1277
|
}
|
|
1070
1278
|
|
|
1071
1279
|
// src/core/capability/paths.ts
|
|
1280
|
+
function applyPatchTargets(patch) {
|
|
1281
|
+
const targets = [];
|
|
1282
|
+
for (const line of patch.split("\n")) {
|
|
1283
|
+
const match = line.match(/^\*\*\* (?:Add|Delete|Update) File: (.+)$/);
|
|
1284
|
+
if (match?.[1]) {
|
|
1285
|
+
targets.push(match[1]);
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1288
|
+
const moveMatch = line.match(/^\*\*\* Move to: (.+)$/);
|
|
1289
|
+
if (moveMatch?.[1]) {
|
|
1290
|
+
targets.push(moveMatch[1]);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
return targets;
|
|
1294
|
+
}
|
|
1295
|
+
function extractToolFilePath(payload) {
|
|
1296
|
+
const toolInput = payload.tool_input;
|
|
1297
|
+
if (!toolInput || typeof toolInput !== "object") {
|
|
1298
|
+
return null;
|
|
1299
|
+
}
|
|
1300
|
+
const input = toolInput;
|
|
1301
|
+
for (const key of ["path", "file_path", "target_file", "filePath"]) {
|
|
1302
|
+
if (typeof input[key] === "string") {
|
|
1303
|
+
return input[key];
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
return null;
|
|
1307
|
+
}
|
|
1308
|
+
function extractToolPatch(payload) {
|
|
1309
|
+
const toolInput = payload.tool_input;
|
|
1310
|
+
if (!toolInput || typeof toolInput !== "object") {
|
|
1311
|
+
return null;
|
|
1312
|
+
}
|
|
1313
|
+
const input = toolInput;
|
|
1314
|
+
for (const key of ["patch", "input", "text"]) {
|
|
1315
|
+
if (typeof input[key] === "string" && input[key].trim()) {
|
|
1316
|
+
return input[key];
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
return null;
|
|
1320
|
+
}
|
|
1321
|
+
function addOutsideRepoPath(paths, token, cwd, repoRoot) {
|
|
1322
|
+
const resolved = resolveMutationTarget(token, cwd);
|
|
1323
|
+
if (resolved && relativeWithinRepo(repoRoot, resolved) === null) {
|
|
1324
|
+
paths.add(resolved);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1072
1327
|
function collectOutsideRepoPaths(command, cwd, repoRoot) {
|
|
1073
1328
|
const tokens = tokenizeShell(command);
|
|
1074
1329
|
const redirects = extractRedirectTargets(tokens);
|
|
1075
1330
|
const paths = /* @__PURE__ */ new Set();
|
|
1076
1331
|
for (const token of tokens.slice(1)) {
|
|
1077
|
-
|
|
1078
|
-
if (resolved && relativeWithinRepo(repoRoot, resolved) === null) {
|
|
1079
|
-
paths.add(resolved);
|
|
1080
|
-
}
|
|
1332
|
+
addOutsideRepoPath(paths, token, cwd, repoRoot);
|
|
1081
1333
|
}
|
|
1082
1334
|
for (const redirect of redirects) {
|
|
1083
|
-
|
|
1084
|
-
|
|
1335
|
+
addOutsideRepoPath(paths, redirect, cwd, repoRoot);
|
|
1336
|
+
}
|
|
1337
|
+
return [...paths];
|
|
1338
|
+
}
|
|
1339
|
+
function collectOutsideRepoPathsFromToolPayload(payload, cwd, repoRoot) {
|
|
1340
|
+
const toolKind = String(payload.tool_name ?? "").trim().toLowerCase();
|
|
1341
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1342
|
+
const filePath = extractToolFilePath(payload);
|
|
1343
|
+
if (filePath) {
|
|
1344
|
+
const resolved = path7.isAbsolute(filePath) ? filePath : path7.resolve(cwd, filePath);
|
|
1345
|
+
if (relativeWithinRepo(repoRoot, resolved) === null) {
|
|
1085
1346
|
paths.add(resolved);
|
|
1086
1347
|
}
|
|
1348
|
+
return [...paths];
|
|
1349
|
+
}
|
|
1350
|
+
if (toolKind === "apply_patch" || toolKind === "applypatch") {
|
|
1351
|
+
const patch = extractToolPatch(payload);
|
|
1352
|
+
if (patch) {
|
|
1353
|
+
for (const target of applyPatchTargets(patch)) {
|
|
1354
|
+
const resolved = path7.isAbsolute(target) ? target : path7.resolve(cwd, target);
|
|
1355
|
+
if (relativeWithinRepo(repoRoot, resolved) === null) {
|
|
1356
|
+
paths.add(resolved);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1087
1360
|
}
|
|
1088
1361
|
return [...paths];
|
|
1089
1362
|
}
|
|
1090
1363
|
|
|
1091
1364
|
// src/core/capability/reasons.ts
|
|
1092
1365
|
var FS_SCOPE_REASONS = /* @__PURE__ */ new Set(["outside_repo_mutation", "outside_repo_redirect"]);
|
|
1366
|
+
var JUDGE_CLOUD_CONSENT_REASON = "judge_cloud_consent";
|
|
1367
|
+
var CAPABILITY_APPROVAL_REASONS = /* @__PURE__ */ new Set([
|
|
1368
|
+
...FS_SCOPE_REASONS,
|
|
1369
|
+
JUDGE_CLOUD_CONSENT_REASON
|
|
1370
|
+
]);
|
|
1093
1371
|
function shouldSkipBrokerApprovedOnce(brokerActive, reason) {
|
|
1094
1372
|
return brokerActive && FS_SCOPE_REASONS.has(reason);
|
|
1095
1373
|
}
|
|
1374
|
+
function shouldSkipBrokerApprovedRecord(brokerActive, approvalReason) {
|
|
1375
|
+
return brokerActive && approvalReason !== void 0 && FS_SCOPE_REASONS.has(approvalReason);
|
|
1376
|
+
}
|
|
1096
1377
|
|
|
1097
1378
|
// src/core/config-layers.ts
|
|
1098
|
-
import
|
|
1379
|
+
import path8 from "node:path";
|
|
1099
1380
|
|
|
1100
1381
|
// src/presets.ts
|
|
1101
1382
|
var CONFIG_PRESETS = {
|
|
@@ -1195,8 +1476,8 @@ function applyConfigPreset(preset, extra = {}) {
|
|
|
1195
1476
|
// src/core/config-layers.ts
|
|
1196
1477
|
function teamConfigPath(homedir = () => process.env.HOME ?? process.env.USERPROFILE ?? "") {
|
|
1197
1478
|
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
1198
|
-
const base = xdg ||
|
|
1199
|
-
return
|
|
1479
|
+
const base = xdg || path8.join(homedir(), ".config");
|
|
1480
|
+
return path8.join(base, "agent-belay", "team.config.json");
|
|
1200
1481
|
}
|
|
1201
1482
|
function applyProtectedLayer(config, builtin) {
|
|
1202
1483
|
const controlPlane = { ...config.controlPlane };
|
|
@@ -1230,6 +1511,10 @@ function resolveLayeredConfig(params) {
|
|
|
1230
1511
|
if (params.teamConfig) {
|
|
1231
1512
|
const teamFile = params.teamConfig;
|
|
1232
1513
|
const teamRaw = teamFile.preset ? applyConfigPreset(teamFile.preset, teamFile.config ?? {}) : teamFile.config ?? params.teamConfig;
|
|
1514
|
+
rejectTeamLayerJudgeSecrets(
|
|
1515
|
+
teamRaw.judge,
|
|
1516
|
+
"team"
|
|
1517
|
+
);
|
|
1233
1518
|
config = mergeConfigLayer(config, asV3Layer(teamRaw));
|
|
1234
1519
|
provenance.push({
|
|
1235
1520
|
path: params.teamConfigPath ?? teamConfigPath(),
|
|
@@ -1475,7 +1760,7 @@ function classifySubagent(payload, repoRoot, options = {}) {
|
|
|
1475
1760
|
}
|
|
1476
1761
|
|
|
1477
1762
|
// src/core/classify-tool.ts
|
|
1478
|
-
import
|
|
1763
|
+
import path16 from "node:path";
|
|
1479
1764
|
|
|
1480
1765
|
// src/core/glob.ts
|
|
1481
1766
|
function matchesSensitivePath(filePath, patterns) {
|
|
@@ -1535,62 +1820,471 @@ function judgeTraceAuditFields(trace) {
|
|
|
1535
1820
|
};
|
|
1536
1821
|
}
|
|
1537
1822
|
|
|
1538
|
-
// src/
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1823
|
+
// src/adapters/layouts/claude.ts
|
|
1824
|
+
import path9 from "node:path";
|
|
1825
|
+
function runnerCommand2(platform, repoRoot, hookName, ...args) {
|
|
1826
|
+
const hooksDir = path9.join(path9.resolve(repoRoot), ".claude", "hooks");
|
|
1827
|
+
return buildRunnerInvocation(platform, hooksDir, repoRoot, hookName, ...args);
|
|
1828
|
+
}
|
|
1829
|
+
var claudeLayout = {
|
|
1830
|
+
name: "claude",
|
|
1831
|
+
configPath(repoRoot) {
|
|
1832
|
+
return path9.join(repoRoot, ".claude", "belay.config.json");
|
|
1833
|
+
},
|
|
1834
|
+
hooksSettingsPath(repoRoot) {
|
|
1835
|
+
return path9.join(repoRoot, ".claude", "settings.json");
|
|
1836
|
+
},
|
|
1837
|
+
hooksDir(repoRoot) {
|
|
1838
|
+
return path9.join(repoRoot, ".claude", "hooks");
|
|
1839
|
+
},
|
|
1840
|
+
runtimeDir(repoRoot) {
|
|
1841
|
+
return path9.join(repoRoot, ".claude", "belay", "runtime");
|
|
1842
|
+
},
|
|
1843
|
+
repoLocalStateDir(repoRoot) {
|
|
1844
|
+
return path9.join(repoRoot, ".claude", "belay");
|
|
1845
|
+
},
|
|
1846
|
+
defaultAuditLogPath(_repoRoot) {
|
|
1847
|
+
return path9.join(".claude", "belay", "audit.ndjson");
|
|
1848
|
+
},
|
|
1849
|
+
repoRootMarkers: [".git", ".claude"],
|
|
1850
|
+
runnerCommand: runnerCommand2,
|
|
1851
|
+
defaultConfig(repoRoot) {
|
|
1852
|
+
return {
|
|
1853
|
+
...DEFAULT_CONFIG_V4,
|
|
1854
|
+
adapter: "claude",
|
|
1855
|
+
audit: {
|
|
1856
|
+
...DEFAULT_CONFIG_V4.audit,
|
|
1857
|
+
logPath: claudeLayout.defaultAuditLogPath(repoRoot)
|
|
1858
|
+
}
|
|
1859
|
+
};
|
|
1549
1860
|
}
|
|
1550
|
-
|
|
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);
|
|
1551
1868
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
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];
|
|
1554
1911
|
}
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
return true;
|
|
1561
|
-
}
|
|
1912
|
+
|
|
1913
|
+
// src/config-io.ts
|
|
1914
|
+
function resolveAdapterName(config) {
|
|
1915
|
+
if (config.adapter === "claude") {
|
|
1916
|
+
return "claude";
|
|
1562
1917
|
}
|
|
1563
|
-
|
|
1918
|
+
if (config.adapter === "codex") {
|
|
1919
|
+
return "codex";
|
|
1920
|
+
}
|
|
1921
|
+
return "cursor";
|
|
1564
1922
|
}
|
|
1565
|
-
function
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1923
|
+
function repoLocalStateDirFor(repoRoot, config) {
|
|
1924
|
+
return getAdapterLayout(resolveAdapterName(config)).repoLocalStateDir(repoRoot);
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
// src/core/credential-store.ts
|
|
1928
|
+
import { chmod, mkdir as mkdir2, readFile as readFile2, unlink, writeFile as writeFile2 } from "node:fs/promises";
|
|
1929
|
+
import path11 from "node:path";
|
|
1930
|
+
var STORE_FILENAME = "credentials.json";
|
|
1931
|
+
function credentialStorePath(stateDir) {
|
|
1932
|
+
return path11.join(stateDir, STORE_FILENAME);
|
|
1933
|
+
}
|
|
1934
|
+
async function readJudgeCredentialStore(stateDir) {
|
|
1935
|
+
const filePath = credentialStorePath(stateDir);
|
|
1936
|
+
try {
|
|
1937
|
+
const raw = await readFile2(filePath, "utf8");
|
|
1938
|
+
const parsed = JSON.parse(raw);
|
|
1939
|
+
const key = parsed.judge?.trim();
|
|
1940
|
+
return key || null;
|
|
1941
|
+
} catch {
|
|
1942
|
+
return null;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
function parseCredentialRef(ref) {
|
|
1946
|
+
if (ref === "store:judge") {
|
|
1947
|
+
return ref;
|
|
1948
|
+
}
|
|
1949
|
+
if (ref.startsWith("env:") && ref.length > 4) {
|
|
1950
|
+
return ref;
|
|
1951
|
+
}
|
|
1952
|
+
return null;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// src/core/judge-runtime-detection.ts
|
|
1956
|
+
import { accessSync, constants } from "node:fs";
|
|
1957
|
+
import path12 from "node:path";
|
|
1958
|
+
var CLI_COMMANDS = {
|
|
1959
|
+
codex: "codex",
|
|
1960
|
+
cursor: "cursor-agent",
|
|
1961
|
+
claude: "claude"
|
|
1962
|
+
};
|
|
1963
|
+
function isVitestRuntime(env) {
|
|
1964
|
+
return Boolean(env.VITEST || env.VITEST_WORKER_ID);
|
|
1965
|
+
}
|
|
1966
|
+
function cliTransportForProvider(providerId) {
|
|
1967
|
+
if (providerId === "ollama") {
|
|
1968
|
+
return null;
|
|
1969
|
+
}
|
|
1970
|
+
return `${providerId}-cli`;
|
|
1971
|
+
}
|
|
1972
|
+
function commandOnPath(command, env) {
|
|
1973
|
+
const pathValue = env.PATH ?? "";
|
|
1974
|
+
for (const dir of pathValue.split(path12.delimiter).filter(Boolean)) {
|
|
1975
|
+
const candidate = path12.join(dir, command);
|
|
1976
|
+
try {
|
|
1977
|
+
accessSync(candidate, constants.X_OK);
|
|
1569
1978
|
return true;
|
|
1979
|
+
} catch {
|
|
1570
1980
|
}
|
|
1571
1981
|
}
|
|
1572
1982
|
return false;
|
|
1573
1983
|
}
|
|
1574
|
-
function
|
|
1575
|
-
const
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
return token;
|
|
1984
|
+
function detectJudgeRuntimeCapabilities(providerId, env = process.env) {
|
|
1985
|
+
const normalized = normalizeLegacyProviderId(String(providerId));
|
|
1986
|
+
if (!normalized || normalized === "ollama") {
|
|
1987
|
+
return { http: false, cliTransport: null };
|
|
1579
1988
|
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
if (segments.length > 1) {
|
|
1583
|
-
return token.replace(basename, "[REDACTED]");
|
|
1989
|
+
if (env.BELAY_JUDGE_DISABLE_CLI_TRANSPORT === "1") {
|
|
1990
|
+
return { http: true, cliTransport: null };
|
|
1584
1991
|
}
|
|
1585
|
-
|
|
1992
|
+
if (isVitestRuntime(env)) {
|
|
1993
|
+
return { http: false, cliTransport: cliTransportForProvider(normalized) };
|
|
1994
|
+
}
|
|
1995
|
+
const command = CLI_COMMANDS[normalized];
|
|
1996
|
+
if (commandOnPath(command, env)) {
|
|
1997
|
+
return { http: false, cliTransport: cliTransportForProvider(normalized) };
|
|
1998
|
+
}
|
|
1999
|
+
return { http: true, cliTransport: null };
|
|
1586
2000
|
}
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
}
|
|
1593
|
-
|
|
2001
|
+
|
|
2002
|
+
// src/core/judge-api-key.ts
|
|
2003
|
+
function resolveFromEnvChain(env, catalogSpec) {
|
|
2004
|
+
const belay = env.BELAY_JUDGE_API_KEY?.trim();
|
|
2005
|
+
if (belay) {
|
|
2006
|
+
return { key: belay, source: "BELAY_JUDGE_API_KEY" };
|
|
2007
|
+
}
|
|
2008
|
+
for (const name of catalogSpec?.apiKeyEnvVars ?? ["OPENAI_API_KEY", "BELAY_JUDGE_API_KEY"]) {
|
|
2009
|
+
const value = env[name]?.trim();
|
|
2010
|
+
if (value) {
|
|
2011
|
+
return { key: value, source: name };
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
return { key: null, source: null };
|
|
2015
|
+
}
|
|
2016
|
+
function sourceKindForProjectMode(fromEnv, judge, catalogSpec) {
|
|
2017
|
+
if (fromEnv.key) {
|
|
2018
|
+
return "env";
|
|
2019
|
+
}
|
|
2020
|
+
const providerId = judge.providerId && normalizeLegacyProviderId(judge.providerId) ? normalizeLegacyProviderId(judge.providerId) : inferProviderIdFromConfig(judge);
|
|
2021
|
+
const spec = catalogSpec ?? getJudgeProviderSpec(providerId);
|
|
2022
|
+
if (spec?.isCloud && detectJudgeRuntimeCapabilities(providerId).cliTransport) {
|
|
2023
|
+
return "host-session";
|
|
2024
|
+
}
|
|
2025
|
+
return "missing";
|
|
2026
|
+
}
|
|
2027
|
+
function sourceKindForApiKeyMode(key, source) {
|
|
2028
|
+
if (!key) {
|
|
2029
|
+
return "missing";
|
|
2030
|
+
}
|
|
2031
|
+
if (source?.startsWith("env:")) {
|
|
2032
|
+
return "env";
|
|
2033
|
+
}
|
|
2034
|
+
return "store";
|
|
2035
|
+
}
|
|
2036
|
+
async function resolveJudgeCredential(params) {
|
|
2037
|
+
const env = params.env ?? process.env;
|
|
2038
|
+
const credential = params.judge.credential ?? { mode: "project" };
|
|
2039
|
+
if (credential.mode === "apiKey") {
|
|
2040
|
+
const ref = credential.ref ? parseCredentialRef(credential.ref) : "store:judge";
|
|
2041
|
+
if (ref === "store:judge") {
|
|
2042
|
+
const stateDir = belayStateDir(params.config, params.repoLocalStateDir);
|
|
2043
|
+
const key = await readJudgeCredentialStore(stateDir);
|
|
2044
|
+
return {
|
|
2045
|
+
key,
|
|
2046
|
+
source: key ? `${stateDir}/credentials.json` : null,
|
|
2047
|
+
sourceKind: sourceKindForApiKeyMode(key, key ? `${stateDir}/credentials.json` : null),
|
|
2048
|
+
mode: "apiKey"
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
if (ref?.startsWith("env:")) {
|
|
2052
|
+
const envName = ref.slice(4);
|
|
2053
|
+
const value = env[envName]?.trim() ?? null;
|
|
2054
|
+
return {
|
|
2055
|
+
key: value,
|
|
2056
|
+
source: value ? `env:${envName}` : null,
|
|
2057
|
+
sourceKind: sourceKindForApiKeyMode(value, value ? `env:${envName}` : null),
|
|
2058
|
+
mode: "apiKey"
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
return { key: null, source: null, sourceKind: "missing", mode: "apiKey" };
|
|
2062
|
+
}
|
|
2063
|
+
const fromEnv = resolveFromEnvChain(env, params.catalogSpec);
|
|
2064
|
+
return {
|
|
2065
|
+
...fromEnv,
|
|
2066
|
+
sourceKind: sourceKindForProjectMode(fromEnv, params.judge, params.catalogSpec),
|
|
2067
|
+
mode: "project"
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
// src/core/judge-config.ts
|
|
2072
|
+
var JUDGE_PROFILE_LOCAL_OLLAMA = resolveJudgeFromCatalog({
|
|
2073
|
+
providerId: "ollama"
|
|
2074
|
+
});
|
|
2075
|
+
var JUDGE_PROFILE_CURSOR = resolveJudgeFromCatalog({
|
|
2076
|
+
providerId: "cursor"
|
|
2077
|
+
});
|
|
2078
|
+
var JUDGE_PROFILE_CLAUDE = resolveJudgeFromCatalog({
|
|
2079
|
+
providerId: "claude"
|
|
2080
|
+
});
|
|
2081
|
+
var JUDGE_PROFILE_CODEX = resolveJudgeFromCatalog({
|
|
2082
|
+
providerId: "codex"
|
|
2083
|
+
});
|
|
2084
|
+
function isCloudJudgeConfig(judge) {
|
|
2085
|
+
if (judge.providerId) {
|
|
2086
|
+
return isCloudProviderId(judge.providerId);
|
|
2087
|
+
}
|
|
2088
|
+
return judge.provider === "openai-compatible" || judge.provider === "anthropic";
|
|
2089
|
+
}
|
|
2090
|
+
function hasValidCloudConsent(judge) {
|
|
2091
|
+
if (!judge.cloudConsent?.accepted) {
|
|
2092
|
+
return false;
|
|
2093
|
+
}
|
|
2094
|
+
if (!judge.endpoint?.trim()) {
|
|
2095
|
+
return false;
|
|
2096
|
+
}
|
|
2097
|
+
const providerId = judge.providerId && normalizeLegacyProviderId(judge.providerId) ? normalizeLegacyProviderId(judge.providerId) : judge.provider === "ollama" ? "ollama" : "codex";
|
|
2098
|
+
return judge.cloudConsent.endpoint === judge.endpoint.trim() && normalizeLegacyProviderId(judge.cloudConsent.providerId) === providerId;
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
// src/core/verdict/containment.ts
|
|
2102
|
+
import path13 from "node:path";
|
|
2103
|
+
|
|
2104
|
+
// src/core/verdict/persistent-paths.ts
|
|
2105
|
+
function isPersistentAgentPath(resolvedPath) {
|
|
2106
|
+
const normalized = resolvedPath.replaceAll("\\", "/");
|
|
2107
|
+
return /\.ssh\/authorized_keys$/i.test(normalized) || /\/\.(zshrc|bashrc|bash_profile|profile)$/i.test(normalized) || /\/\.config\//i.test(normalized) || /\/crontab$/i.test(normalized) || /LaunchAgents\//i.test(normalized) || /\/Library\/LaunchDaemons\//i.test(normalized);
|
|
2108
|
+
}
|
|
2109
|
+
function isOutsideRepoSecretCredentialPath(resolvedPath) {
|
|
2110
|
+
const normalized = resolvedPath.replaceAll("\\", "/");
|
|
2111
|
+
return /\/\.env(?:\.[^/]+)?$/i.test(normalized) || /\/[^/]+\.pem$/i.test(normalized) || /\/\.ssh\/id_[^/]+$/i.test(normalized) || /\/\.aws\//i.test(normalized) || /\/\.npmrc$/i.test(normalized) || /\/\.git-credentials$/i.test(normalized) || /\/\.netrc$/i.test(normalized) || /\/\.kube\/config$/i.test(normalized) || /\/\.docker\/config\.json$/i.test(normalized) || /\/\.gnupg\//i.test(normalized) || /\/\.pypirc$/i.test(normalized);
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
// src/core/verdict/containment.ts
|
|
2115
|
+
function expandHome(token) {
|
|
2116
|
+
if (token === "~" || token.startsWith("~/")) {
|
|
2117
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
2118
|
+
if (!home) {
|
|
2119
|
+
return token;
|
|
2120
|
+
}
|
|
2121
|
+
return token === "~" ? home : path13.join(home, token.slice(2));
|
|
2122
|
+
}
|
|
2123
|
+
return token;
|
|
2124
|
+
}
|
|
2125
|
+
function resolveTrustedPath(token, trustedCwd, trusted) {
|
|
2126
|
+
if (!token || token === "--" || token.startsWith("-")) {
|
|
2127
|
+
return null;
|
|
2128
|
+
}
|
|
2129
|
+
if (!trusted || !trustedCwd) {
|
|
2130
|
+
return null;
|
|
2131
|
+
}
|
|
2132
|
+
const expanded = expandHome(token);
|
|
2133
|
+
if (path13.isAbsolute(expanded)) {
|
|
2134
|
+
return canonicalPath(expanded);
|
|
2135
|
+
}
|
|
2136
|
+
return canonicalPath(path13.resolve(trustedCwd, expanded));
|
|
2137
|
+
}
|
|
2138
|
+
function locationForPath(resolvedPath, repoRoot) {
|
|
2139
|
+
if (!resolvedPath) {
|
|
2140
|
+
return "unknown";
|
|
2141
|
+
}
|
|
2142
|
+
if (pathWithinRoot(repoRoot, resolvedPath)) {
|
|
2143
|
+
return "repo_local";
|
|
2144
|
+
}
|
|
2145
|
+
return "repo_outside";
|
|
2146
|
+
}
|
|
2147
|
+
function isGitPath(resolvedPath, repoRoot) {
|
|
2148
|
+
const relative = relativeWithinRepo(repoRoot, resolvedPath);
|
|
2149
|
+
if (!relative) {
|
|
2150
|
+
return false;
|
|
2151
|
+
}
|
|
2152
|
+
const normalized = relative.replaceAll("\\", "/");
|
|
2153
|
+
return normalized === ".git" || normalized.startsWith(".git/");
|
|
2154
|
+
}
|
|
2155
|
+
function isHighStakesPath(resolvedPath, repoRoot, sensitivePaths, protectedRoots = []) {
|
|
2156
|
+
if (isGitPath(resolvedPath, repoRoot)) {
|
|
2157
|
+
return true;
|
|
2158
|
+
}
|
|
2159
|
+
const relative = relativeWithinRepo(repoRoot, resolvedPath);
|
|
2160
|
+
if (relative !== null && matchesSensitivePath(relative.replaceAll("\\", "/"), sensitivePaths)) {
|
|
2161
|
+
return true;
|
|
2162
|
+
}
|
|
2163
|
+
if (relative === null && isOutsideRepoSecretCredentialPath(resolvedPath)) {
|
|
2164
|
+
return true;
|
|
2165
|
+
}
|
|
2166
|
+
return protectedRoots.some((root) => pathWithinRoot(root, resolvedPath));
|
|
2167
|
+
}
|
|
2168
|
+
var DESTRUCTIVE_MUTATION_HEADS = /* @__PURE__ */ new Set([
|
|
2169
|
+
"rm",
|
|
2170
|
+
"shred",
|
|
2171
|
+
"truncate",
|
|
2172
|
+
"dd",
|
|
2173
|
+
"chmod",
|
|
2174
|
+
"chown",
|
|
2175
|
+
"mv",
|
|
2176
|
+
"cp"
|
|
2177
|
+
]);
|
|
2178
|
+
function isDestructiveMutationHead(head) {
|
|
2179
|
+
return DESTRUCTIVE_MUTATION_HEADS.has(head);
|
|
2180
|
+
}
|
|
2181
|
+
function touchesProtectedRoot(resolvedPath, protectedRoots) {
|
|
2182
|
+
return protectedRoots.some((root) => pathWithinRoot(root, resolvedPath) || root === resolvedPath);
|
|
2183
|
+
}
|
|
2184
|
+
function isDestructiveHighStakesMutation(head, resolvedPath, repoRoot, sensitivePaths, protectedRoots = []) {
|
|
2185
|
+
if (touchesProtectedRoot(resolvedPath, protectedRoots)) {
|
|
2186
|
+
return true;
|
|
2187
|
+
}
|
|
2188
|
+
if (!isDestructiveMutationHead(head)) {
|
|
2189
|
+
return false;
|
|
2190
|
+
}
|
|
2191
|
+
return isHighStakesPath(resolvedPath, repoRoot, sensitivePaths, []);
|
|
2192
|
+
}
|
|
2193
|
+
function analyzePathTargets(params) {
|
|
2194
|
+
const signals = [];
|
|
2195
|
+
if (!params.trustedCwd || !params.cwd) {
|
|
2196
|
+
return {
|
|
2197
|
+
location: "unknown",
|
|
2198
|
+
isHighStakes: false,
|
|
2199
|
+
signals: ["missing_trusted_cwd"]
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
const locations = /* @__PURE__ */ new Set();
|
|
2203
|
+
let isHighStakes = false;
|
|
2204
|
+
for (const target of params.targets) {
|
|
2205
|
+
const resolved = resolveTrustedPath(target, params.cwd, params.trustedCwd) ?? resolveMutationTarget(target, params.cwd);
|
|
2206
|
+
const location2 = locationForPath(resolved, params.repoRoot);
|
|
2207
|
+
locations.add(location2);
|
|
2208
|
+
if (resolved && isHighStakesPath(
|
|
2209
|
+
resolved,
|
|
2210
|
+
params.repoRoot,
|
|
2211
|
+
params.sensitivePaths,
|
|
2212
|
+
params.protectedArtifactRoots
|
|
2213
|
+
)) {
|
|
2214
|
+
isHighStakes = true;
|
|
2215
|
+
signals.push("high_stakes_path");
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
let location = "unknown";
|
|
2219
|
+
if (locations.size === 0) {
|
|
2220
|
+
location = "unknown";
|
|
2221
|
+
} else if (locations.size === 1) {
|
|
2222
|
+
location = [...locations][0] ?? "unknown";
|
|
2223
|
+
} else {
|
|
2224
|
+
location = "mixed";
|
|
2225
|
+
}
|
|
2226
|
+
return { location, isHighStakes, signals };
|
|
2227
|
+
}
|
|
2228
|
+
function cwdRelative(repoRoot, cwd) {
|
|
2229
|
+
return relativeWithinRepo(repoRoot, cwd) ?? cwd;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
// src/core/verdict/judge-outbound.ts
|
|
2233
|
+
var PATH_LIKE = /(?:^|[\s"'`=])(~\/[^\s"'`]+|\/[^\s"'`]+|\.\/[^\s"'`]+|\.\.\/[^\s"'`]+|[A-Za-z]:\\[^\s"'`]+)/g;
|
|
2234
|
+
var REDACTED_PLACEHOLDER = /^(?:<redacted>|\[REDACTED\]|<secret>|<high-entropy>|<approval-id>)$/i;
|
|
2235
|
+
var URL_CREDENTIALS_PATTERN2 = /\b[A-Za-z][A-Za-z0-9+.-]*:\/\/([^/\s:@]+):([^@\s/]+)@/gi;
|
|
2236
|
+
var GENERIC_AUTH_HEADER_PATTERN2 = /\b(?:Authorization|X-Api-Key|X-Auth-Token|Private-Token):\s*([^\s]+)/gi;
|
|
2237
|
+
function hasResidualBearerToken(text) {
|
|
2238
|
+
for (const match of text.matchAll(/\bBearer\s+(\S+)/gi)) {
|
|
2239
|
+
const token = match[1] ?? "";
|
|
2240
|
+
if (!REDACTED_PLACEHOLDER.test(token)) {
|
|
2241
|
+
return true;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
return false;
|
|
2245
|
+
}
|
|
2246
|
+
function hasResidualApiKey(text) {
|
|
2247
|
+
return /\bsk-(?![^\s]*<redacted>)[A-Za-z0-9_-]{4,}/i.test(text);
|
|
2248
|
+
}
|
|
2249
|
+
function hasResidualUrlCredentials(text) {
|
|
2250
|
+
for (const match of text.matchAll(URL_CREDENTIALS_PATTERN2)) {
|
|
2251
|
+
const username = (match[1] ?? "").replace(/^['"]|['"]$/g, "");
|
|
2252
|
+
const password = (match[2] ?? "").replace(/^['"]|['"]$/g, "");
|
|
2253
|
+
if (!REDACTED_PLACEHOLDER.test(username) || !REDACTED_PLACEHOLDER.test(password)) {
|
|
2254
|
+
return true;
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
return false;
|
|
2258
|
+
}
|
|
2259
|
+
function hasResidualAuthHeader(text) {
|
|
2260
|
+
for (const match of text.matchAll(GENERIC_AUTH_HEADER_PATTERN2)) {
|
|
2261
|
+
const token = (match[1] ?? "").replace(/^['"]|['"]$/g, "");
|
|
2262
|
+
if (!REDACTED_PLACEHOLDER.test(token)) {
|
|
2263
|
+
return true;
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
return false;
|
|
2267
|
+
}
|
|
2268
|
+
function redactSensitivePathToken(token, sensitivePaths) {
|
|
2269
|
+
const trimmed = token.replace(/^['"`]+|['"`]+$/g, "");
|
|
2270
|
+
const normalized = trimmed.replaceAll("\\", "/");
|
|
2271
|
+
if (!matchesSensitivePath(normalized, sensitivePaths)) {
|
|
2272
|
+
return token;
|
|
2273
|
+
}
|
|
2274
|
+
const segments = normalized.split("/");
|
|
2275
|
+
const basename = segments.at(-1) ?? normalized;
|
|
2276
|
+
if (segments.length > 1) {
|
|
2277
|
+
return token.replace(basename, "[REDACTED]");
|
|
2278
|
+
}
|
|
2279
|
+
return "[REDACTED]";
|
|
2280
|
+
}
|
|
2281
|
+
function scrubOutboundForJudge(text, options) {
|
|
2282
|
+
try {
|
|
2283
|
+
let scrubbed = scrubString(text, {
|
|
2284
|
+
...options.scrubOptions,
|
|
2285
|
+
maskHighEntropyStrings: options.scrubOptions.maskHighEntropyStrings !== false
|
|
2286
|
+
});
|
|
2287
|
+
scrubbed = scrubbed.replace(PATH_LIKE, (match, pathToken) => {
|
|
1594
2288
|
const redacted = redactSensitivePathToken(pathToken, options.sensitivePaths);
|
|
1595
2289
|
return match.replace(pathToken, redacted);
|
|
1596
2290
|
});
|
|
@@ -1608,33 +2302,53 @@ function scrubOutboundForJudge(text, options) {
|
|
|
1608
2302
|
var SECRET_PATTERNS = [/\.env\b/i, /\.pem\b/i, /id_rsa\b/i, /credentials/i, /secrets?\b/i];
|
|
1609
2303
|
var DESTRUCTIVE_VERBS = /\b(rm|rmtree|unlink|delete|truncate|shred|destroy|drop)\b/i;
|
|
1610
2304
|
var GIT_PATTERNS = /\.git\b/i;
|
|
1611
|
-
var TIER1_PROMPT = `Answer ONLY with JSON: {"
|
|
2305
|
+
var TIER1_PROMPT = `Answer ONLY with JSON: {"local_recoverable":boolean,"destroys_history_or_secrets":boolean,"reason":string}
|
|
1612
2306
|
reason should be a short snake_case label.
|
|
1613
2307
|
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
2308
|
+
local_recoverable: true if a mistaken operation can be undone locally (git revert, fs snapshot, trivial manual revert).
|
|
2309
|
+
local_recoverable: false if remote/destructive (git push --force, aws s3 rm, secret send) OR persistent local harm (~/.ssh/authorized_keys, shell rc, crontab, ~/.config persistence).
|
|
2310
|
+
|
|
2311
|
+
destroys_history_or_secrets: true if the operation deletes or corrupts git history (.git), overwrites secrets/credentials, or modifies persistent agent startup (shell rc, crontab, launchd, authorized_keys).
|
|
2312
|
+
|
|
2313
|
+
Examples local_recoverable=true, destroys_history_or_secrets=false: IDE plan files under the user home, /tmp files, in-repo edits, READ/QUERY/GET.
|
|
2314
|
+
Examples local_recoverable=false OR destroys_history_or_secrets=true: remote mutations, credential paths, agent startup persistence, rm -rf .git.
|
|
2315
|
+
If genuinely ambiguous about recoverability or persistent harm, answer false.
|
|
1617
2316
|
|
|
1618
2317
|
Command/code:
|
|
1619
2318
|
`;
|
|
1620
2319
|
function failClosedVerdict(reason) {
|
|
1621
2320
|
return {
|
|
1622
|
-
|
|
2321
|
+
local_recoverable: false,
|
|
1623
2322
|
destroys_outside_repo: true,
|
|
1624
2323
|
destroys_history_or_secrets: true,
|
|
2324
|
+
external_change: true,
|
|
1625
2325
|
reason
|
|
1626
2326
|
};
|
|
1627
2327
|
}
|
|
2328
|
+
function parseLocalRecoverable(parsed) {
|
|
2329
|
+
if (typeof parsed.local_recoverable === "boolean") {
|
|
2330
|
+
return parsed.local_recoverable;
|
|
2331
|
+
}
|
|
2332
|
+
if (typeof parsed.external_change === "boolean") {
|
|
2333
|
+
return !parsed.external_change;
|
|
2334
|
+
}
|
|
2335
|
+
return null;
|
|
2336
|
+
}
|
|
2337
|
+
function buildTier1Prompt(text) {
|
|
2338
|
+
return `${TIER1_PROMPT}${text}`;
|
|
2339
|
+
}
|
|
1628
2340
|
function parseTier1Json(raw) {
|
|
1629
2341
|
try {
|
|
1630
2342
|
const parsed = JSON.parse(raw);
|
|
1631
|
-
|
|
2343
|
+
const localRecoverable = parseLocalRecoverable(parsed);
|
|
2344
|
+
if (localRecoverable === null) {
|
|
1632
2345
|
return null;
|
|
1633
2346
|
}
|
|
1634
2347
|
return {
|
|
2348
|
+
local_recoverable: localRecoverable,
|
|
2349
|
+
destroys_outside_repo: typeof parsed.destroys_outside_repo === "boolean" ? parsed.destroys_outside_repo : false,
|
|
2350
|
+
destroys_history_or_secrets: typeof parsed.destroys_history_or_secrets === "boolean" ? parsed.destroys_history_or_secrets : false,
|
|
1635
2351
|
external_change: parsed.external_change,
|
|
1636
|
-
destroys_outside_repo: false,
|
|
1637
|
-
destroys_history_or_secrets: false,
|
|
1638
2352
|
reason: typeof parsed.reason === "string" ? parsed.reason : "tier1_llm"
|
|
1639
2353
|
};
|
|
1640
2354
|
} catch {
|
|
@@ -1648,7 +2362,7 @@ function prescanInterpreterCode(code) {
|
|
|
1648
2362
|
const hitsDestructive = DESTRUCTIVE_VERBS.test(normalized);
|
|
1649
2363
|
if ((hitsSecret || hitsGit) && hitsDestructive) {
|
|
1650
2364
|
return {
|
|
1651
|
-
|
|
2365
|
+
local_recoverable: true,
|
|
1652
2366
|
destroys_outside_repo: false,
|
|
1653
2367
|
destroys_history_or_secrets: true,
|
|
1654
2368
|
reason: "prescan_destructive_secret"
|
|
@@ -1656,11 +2370,49 @@ function prescanInterpreterCode(code) {
|
|
|
1656
2370
|
}
|
|
1657
2371
|
return null;
|
|
1658
2372
|
}
|
|
2373
|
+
function prescanMutationTargets(params) {
|
|
2374
|
+
for (const target of params.targets) {
|
|
2375
|
+
const resolved = resolveTrustedPath(target, params.cwd, params.trustedCwd) ?? resolveMutationTarget(target, params.cwd);
|
|
2376
|
+
if (!resolved) {
|
|
2377
|
+
continue;
|
|
2378
|
+
}
|
|
2379
|
+
const relative = relativeWithinRepo(params.repoRoot, resolved);
|
|
2380
|
+
if (relative !== null && matchesSensitivePath(relative.replaceAll("\\", "/"), params.sensitivePaths)) {
|
|
2381
|
+
return {
|
|
2382
|
+
local_recoverable: false,
|
|
2383
|
+
destroys_outside_repo: false,
|
|
2384
|
+
destroys_history_or_secrets: true,
|
|
2385
|
+
reason: "sensitive_path_mutation"
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
if (relative === null && isOutsideRepoSecretCredentialPath(resolved)) {
|
|
2389
|
+
return {
|
|
2390
|
+
local_recoverable: false,
|
|
2391
|
+
destroys_outside_repo: false,
|
|
2392
|
+
destroys_history_or_secrets: true,
|
|
2393
|
+
reason: "outside_repo_secret_credential_path"
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
if (isPersistentAgentPath(resolved)) {
|
|
2397
|
+
return {
|
|
2398
|
+
local_recoverable: false,
|
|
2399
|
+
destroys_outside_repo: false,
|
|
2400
|
+
destroys_history_or_secrets: true,
|
|
2401
|
+
reason: "persistent_agent_path"
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
return null;
|
|
2406
|
+
}
|
|
2407
|
+
function mutationPrescanRequiresAsk(params) {
|
|
2408
|
+
const prescan = prescanMutationTargets(params);
|
|
2409
|
+
return prescan && tier1RequiresAsk(prescan) ? prescan : null;
|
|
2410
|
+
}
|
|
1659
2411
|
function createDeterministicJudgeStub() {
|
|
1660
2412
|
return {
|
|
1661
2413
|
evaluate() {
|
|
1662
2414
|
return Promise.resolve({
|
|
1663
|
-
|
|
2415
|
+
local_recoverable: true,
|
|
1664
2416
|
destroys_outside_repo: false,
|
|
1665
2417
|
destroys_history_or_secrets: false,
|
|
1666
2418
|
reason: "deterministic_stub"
|
|
@@ -1792,8 +2544,7 @@ function createOpenAiCompatibleJudge(options) {
|
|
|
1792
2544
|
};
|
|
1793
2545
|
return failClosedVerdict("outbound_scrub_failed");
|
|
1794
2546
|
}
|
|
1795
|
-
const
|
|
1796
|
-
const resolvedKey = resolveJudgeApiKey2();
|
|
2547
|
+
const resolvedKey = options.resolveApiKey ? await options.resolveApiKey() : { key: options.apiKey ?? null, source: null };
|
|
1797
2548
|
const apiKey = options.apiKey ?? resolvedKey.key;
|
|
1798
2549
|
if (!apiKey) {
|
|
1799
2550
|
judge.lastTrace = {
|
|
@@ -1857,149 +2608,418 @@ function createOpenAiCompatibleJudge(options) {
|
|
|
1857
2608
|
return judge;
|
|
1858
2609
|
}
|
|
1859
2610
|
function tier1RequiresAsk(verdict2) {
|
|
1860
|
-
return verdict2.
|
|
2611
|
+
return !verdict2.local_recoverable || verdict2.destroys_history_or_secrets;
|
|
1861
2612
|
}
|
|
1862
2613
|
|
|
1863
|
-
// src/core/verdict/judge-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
2614
|
+
// src/core/verdict/judge-cli.ts
|
|
2615
|
+
import { spawn } from "node:child_process";
|
|
2616
|
+
var CLI_BINARIES = {
|
|
2617
|
+
codex: "codex",
|
|
2618
|
+
cursor: "cursor-agent",
|
|
2619
|
+
claude: "claude"
|
|
2620
|
+
};
|
|
2621
|
+
function failClosedVerdict2(reason) {
|
|
2622
|
+
return {
|
|
2623
|
+
local_recoverable: false,
|
|
2624
|
+
destroys_outside_repo: true,
|
|
2625
|
+
destroys_history_or_secrets: true,
|
|
2626
|
+
external_change: true,
|
|
2627
|
+
reason
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
function transportForProvider(providerId) {
|
|
2631
|
+
return `${providerId}-cli`;
|
|
2632
|
+
}
|
|
2633
|
+
function traceProvider(providerId) {
|
|
2634
|
+
if (providerId === "claude") {
|
|
2635
|
+
return "anthropic";
|
|
2636
|
+
}
|
|
2637
|
+
return "openai-compatible";
|
|
2638
|
+
}
|
|
2639
|
+
function initialTrace(options) {
|
|
2640
|
+
return {
|
|
2641
|
+
provider: traceProvider(options.providerId),
|
|
2642
|
+
modelRequested: options.modelRequested,
|
|
2643
|
+
modelResolved: options.modelResolved,
|
|
2644
|
+
latencyMs: 0,
|
|
2645
|
+
transport: transportForProvider(options.providerId)
|
|
2646
|
+
};
|
|
2647
|
+
}
|
|
2648
|
+
function buildCliInvocation(providerId, prompt, model) {
|
|
2649
|
+
if (providerId === "codex") {
|
|
1868
2650
|
return {
|
|
1869
|
-
|
|
1870
|
-
|
|
2651
|
+
binary: CLI_BINARIES[providerId],
|
|
2652
|
+
args: [
|
|
2653
|
+
"exec",
|
|
2654
|
+
"--json",
|
|
2655
|
+
"--skip-git-repo-check",
|
|
2656
|
+
"--sandbox",
|
|
2657
|
+
"read-only",
|
|
2658
|
+
"--model",
|
|
2659
|
+
model,
|
|
2660
|
+
"-"
|
|
2661
|
+
],
|
|
2662
|
+
stdin: prompt
|
|
1871
2663
|
};
|
|
1872
2664
|
}
|
|
1873
|
-
|
|
2665
|
+
if (providerId === "cursor") {
|
|
2666
|
+
return {
|
|
2667
|
+
binary: CLI_BINARIES[providerId],
|
|
2668
|
+
args: [
|
|
2669
|
+
"--print",
|
|
2670
|
+
"--output-format",
|
|
2671
|
+
"json",
|
|
2672
|
+
"--mode",
|
|
2673
|
+
"ask",
|
|
2674
|
+
"--sandbox",
|
|
2675
|
+
"enabled",
|
|
2676
|
+
"--trust",
|
|
2677
|
+
"--model",
|
|
2678
|
+
model,
|
|
2679
|
+
prompt
|
|
2680
|
+
]
|
|
2681
|
+
};
|
|
2682
|
+
}
|
|
2683
|
+
return {
|
|
2684
|
+
binary: CLI_BINARIES[providerId],
|
|
2685
|
+
args: [
|
|
2686
|
+
"-p",
|
|
2687
|
+
"--output-format",
|
|
2688
|
+
"json",
|
|
2689
|
+
"--permission-mode",
|
|
2690
|
+
"plan",
|
|
2691
|
+
"--tools",
|
|
2692
|
+
"",
|
|
2693
|
+
"--bare",
|
|
2694
|
+
"--model",
|
|
2695
|
+
model
|
|
2696
|
+
],
|
|
2697
|
+
stdin: prompt
|
|
2698
|
+
};
|
|
1874
2699
|
}
|
|
1875
|
-
function
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
2700
|
+
function tryParseJson(raw) {
|
|
2701
|
+
try {
|
|
2702
|
+
return JSON.parse(raw);
|
|
2703
|
+
} catch {
|
|
2704
|
+
return null;
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
function extractVerdictFromJsonValue(value) {
|
|
2708
|
+
if (typeof value === "string") {
|
|
2709
|
+
return parseTier1Json(value.trim());
|
|
2710
|
+
}
|
|
2711
|
+
if (Array.isArray(value)) {
|
|
2712
|
+
for (let index = value.length - 1; index >= 0; index -= 1) {
|
|
2713
|
+
const verdict2 = extractVerdictFromJsonValue(value[index]);
|
|
2714
|
+
if (verdict2) {
|
|
2715
|
+
return verdict2;
|
|
2716
|
+
}
|
|
1887
2717
|
}
|
|
1888
|
-
|
|
1889
|
-
const { resolved } = resolveCloudModel(judgeConfig.model, pinned);
|
|
1890
|
-
return createOpenAiCompatibleJudge({
|
|
1891
|
-
endpoint,
|
|
1892
|
-
modelRequested: judgeConfig.model,
|
|
1893
|
-
modelResolved: resolved,
|
|
1894
|
-
timeoutMs: judgeConfig.timeoutMs,
|
|
1895
|
-
sensitivePaths: config.classifier.sensitivePaths,
|
|
1896
|
-
scrubOptions: scrubOptionsFromConfig(config)
|
|
1897
|
-
});
|
|
2718
|
+
return null;
|
|
1898
2719
|
}
|
|
1899
|
-
if (
|
|
1900
|
-
return
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
2720
|
+
if (!value || typeof value !== "object") {
|
|
2721
|
+
return null;
|
|
2722
|
+
}
|
|
2723
|
+
const record = value;
|
|
2724
|
+
for (const key of ["result", "content", "text", "message", "output_text", "final"]) {
|
|
2725
|
+
if (!(key in record)) {
|
|
2726
|
+
continue;
|
|
2727
|
+
}
|
|
2728
|
+
const verdict2 = extractVerdictFromJsonValue(record[key]);
|
|
2729
|
+
if (verdict2) {
|
|
2730
|
+
return verdict2;
|
|
2731
|
+
}
|
|
1906
2732
|
}
|
|
1907
|
-
|
|
2733
|
+
for (const candidate of Object.values(record).reverse()) {
|
|
2734
|
+
const verdict2 = extractVerdictFromJsonValue(candidate);
|
|
2735
|
+
if (verdict2) {
|
|
2736
|
+
return verdict2;
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
return null;
|
|
1908
2740
|
}
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
return token;
|
|
2741
|
+
function parseCliJsonLines(raw) {
|
|
2742
|
+
const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
2743
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
2744
|
+
const parsed = tryParseJson(lines[index] ?? "");
|
|
2745
|
+
if (!parsed) {
|
|
2746
|
+
continue;
|
|
2747
|
+
}
|
|
2748
|
+
const verdict2 = extractVerdictFromJsonValue(parsed);
|
|
2749
|
+
if (verdict2) {
|
|
2750
|
+
return verdict2;
|
|
1920
2751
|
}
|
|
1921
|
-
return token === "~" ? home : path8.join(home, token.slice(2));
|
|
1922
2752
|
}
|
|
1923
|
-
return
|
|
2753
|
+
return null;
|
|
1924
2754
|
}
|
|
1925
|
-
function
|
|
1926
|
-
|
|
1927
|
-
|
|
2755
|
+
function parseCliJudgeOutput(providerId, raw) {
|
|
2756
|
+
const direct = parseTier1Json(raw);
|
|
2757
|
+
if (direct) {
|
|
2758
|
+
return direct;
|
|
1928
2759
|
}
|
|
1929
|
-
|
|
1930
|
-
|
|
2760
|
+
const parsed = tryParseJson(raw);
|
|
2761
|
+
if (parsed) {
|
|
2762
|
+
const envelopeVerdict = extractVerdictFromJsonValue(parsed);
|
|
2763
|
+
if (envelopeVerdict) {
|
|
2764
|
+
return envelopeVerdict;
|
|
2765
|
+
}
|
|
1931
2766
|
}
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
return canonicalPath(expanded);
|
|
2767
|
+
if (providerId === "codex") {
|
|
2768
|
+
return parseCliJsonLines(raw);
|
|
1935
2769
|
}
|
|
1936
|
-
return
|
|
2770
|
+
return parseCliJsonLines(raw);
|
|
1937
2771
|
}
|
|
1938
|
-
function
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
2772
|
+
async function runCliJson(invocation, timeoutMs) {
|
|
2773
|
+
const { binary, args, stdin } = invocation;
|
|
2774
|
+
return new Promise((resolve, reject) => {
|
|
2775
|
+
const child = spawn(binary, args, {
|
|
2776
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2777
|
+
});
|
|
2778
|
+
let stdout = "";
|
|
2779
|
+
let stderr = "";
|
|
2780
|
+
const timer = setTimeout(() => {
|
|
2781
|
+
child.kill("SIGTERM");
|
|
2782
|
+
reject(new Error(`${binary} timed out`));
|
|
2783
|
+
}, timeoutMs);
|
|
2784
|
+
child.stdout.on("data", (chunk) => {
|
|
2785
|
+
stdout += String(chunk);
|
|
2786
|
+
});
|
|
2787
|
+
child.stderr.on("data", (chunk) => {
|
|
2788
|
+
stderr += String(chunk);
|
|
2789
|
+
});
|
|
2790
|
+
if (stdin !== void 0) {
|
|
2791
|
+
child.stdin.write(stdin);
|
|
2792
|
+
}
|
|
2793
|
+
child.stdin.end();
|
|
2794
|
+
child.on("error", (error) => {
|
|
2795
|
+
clearTimeout(timer);
|
|
2796
|
+
reject(error);
|
|
2797
|
+
});
|
|
2798
|
+
child.on("close", (code) => {
|
|
2799
|
+
clearTimeout(timer);
|
|
2800
|
+
if (code === 0 && stdout.trim()) {
|
|
2801
|
+
resolve(stdout.trim());
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
reject(new Error(stderr.trim() || `${binary} exited with code ${code ?? "unknown"}`));
|
|
2805
|
+
});
|
|
2806
|
+
});
|
|
2807
|
+
}
|
|
2808
|
+
function createCliJudge(options) {
|
|
2809
|
+
const transport = transportForProvider(options.providerId);
|
|
2810
|
+
const timeoutMs = options.timeoutMs ?? 25e3;
|
|
2811
|
+
const providerId = options.providerId;
|
|
2812
|
+
const runCommand = options.runCliCommand ?? runCliJson;
|
|
2813
|
+
const judge = {
|
|
2814
|
+
lastTrace: initialTrace(options),
|
|
2815
|
+
async evaluate(input) {
|
|
2816
|
+
const started = Date.now();
|
|
2817
|
+
const prescan = input.innerCode ? prescanInterpreterCode(input.innerCode) : null;
|
|
2818
|
+
if (prescan?.destroys_history_or_secrets) {
|
|
2819
|
+
judge.lastTrace = {
|
|
2820
|
+
provider: traceProvider(options.providerId),
|
|
2821
|
+
modelRequested: options.modelRequested,
|
|
2822
|
+
modelResolved: options.modelResolved,
|
|
2823
|
+
latencyMs: Date.now() - started,
|
|
2824
|
+
transport
|
|
2825
|
+
};
|
|
2826
|
+
return prescan;
|
|
2827
|
+
}
|
|
2828
|
+
const scrubbed = scrubOutboundForJudge(input.text, {
|
|
2829
|
+
sensitivePaths: options.sensitivePaths,
|
|
2830
|
+
scrubOptions: options.scrubOptions
|
|
2831
|
+
});
|
|
2832
|
+
if (!scrubbed.ok) {
|
|
2833
|
+
judge.lastTrace = {
|
|
2834
|
+
provider: "fallback",
|
|
2835
|
+
modelRequested: options.modelRequested,
|
|
2836
|
+
modelResolved: options.modelResolved,
|
|
2837
|
+
latencyMs: Date.now() - started,
|
|
2838
|
+
fallbackReason: scrubbed.reason
|
|
2839
|
+
};
|
|
2840
|
+
return failClosedVerdict2("outbound_scrub_failed");
|
|
2841
|
+
}
|
|
2842
|
+
const prompt = buildTier1Prompt(scrubbed.text);
|
|
2843
|
+
try {
|
|
2844
|
+
const raw = await runCommand(
|
|
2845
|
+
buildCliInvocation(providerId, prompt, options.modelResolved),
|
|
2846
|
+
timeoutMs
|
|
2847
|
+
);
|
|
2848
|
+
const parsed = parseCliJudgeOutput(providerId, raw);
|
|
2849
|
+
if (!parsed) {
|
|
2850
|
+
judge.lastTrace = {
|
|
2851
|
+
provider: "fallback",
|
|
2852
|
+
modelRequested: options.modelRequested,
|
|
2853
|
+
modelResolved: options.modelResolved,
|
|
2854
|
+
latencyMs: Date.now() - started,
|
|
2855
|
+
fallbackReason: `${providerId}_cli_parse_error`
|
|
2856
|
+
};
|
|
2857
|
+
return failClosedVerdict2(`${providerId}_cli_parse_error`);
|
|
2858
|
+
}
|
|
2859
|
+
judge.lastTrace = {
|
|
2860
|
+
provider: traceProvider(options.providerId),
|
|
2861
|
+
modelRequested: options.modelRequested,
|
|
2862
|
+
modelResolved: options.modelResolved,
|
|
2863
|
+
latencyMs: Date.now() - started,
|
|
2864
|
+
transport,
|
|
2865
|
+
outboundRedacted: true
|
|
2866
|
+
};
|
|
2867
|
+
return parsed;
|
|
2868
|
+
} catch {
|
|
2869
|
+
judge.lastTrace = {
|
|
2870
|
+
provider: "fallback",
|
|
2871
|
+
modelRequested: options.modelRequested,
|
|
2872
|
+
modelResolved: options.modelResolved,
|
|
2873
|
+
latencyMs: Date.now() - started,
|
|
2874
|
+
fallbackReason: `${providerId}_cli_unavailable`
|
|
2875
|
+
};
|
|
2876
|
+
return failClosedVerdict2(`${providerId}_cli_unavailable`);
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
};
|
|
2880
|
+
return judge;
|
|
2881
|
+
}
|
|
2882
|
+
function createCodexCliJudge(options) {
|
|
2883
|
+
return createCliJudge({ ...options, providerId: "codex" });
|
|
2884
|
+
}
|
|
2885
|
+
function createCursorCliJudge(options) {
|
|
2886
|
+
return createCliJudge({ ...options, providerId: "cursor" });
|
|
2887
|
+
}
|
|
2888
|
+
function createClaudeCliJudge(options) {
|
|
2889
|
+
return createCliJudge({ ...options, providerId: "claude" });
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
// src/core/verdict/judge-factory.ts
|
|
2893
|
+
var FIXTURE_MODELS_URL = new URL("../../../fixtures/judge-models.json", import.meta.url);
|
|
2894
|
+
function resolveJudgeModel(judge) {
|
|
2895
|
+
rejectDeprecatedJudgeModelAuto(judge.model);
|
|
2896
|
+
const requested = judge.model;
|
|
2897
|
+
const envOverride = testJudgeModelOverride();
|
|
2898
|
+
return { requested, resolved: envOverride || requested };
|
|
2899
|
+
}
|
|
2900
|
+
function testJudgeModelOverride() {
|
|
2901
|
+
if (!process.env.VITEST && !process.env.VITEST_WORKER_ID) {
|
|
2902
|
+
return void 0;
|
|
1944
2903
|
}
|
|
1945
|
-
return
|
|
2904
|
+
return process.env.BELAY_JUDGE_MODEL_RESOLVED?.trim() || void 0;
|
|
1946
2905
|
}
|
|
1947
|
-
function
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
2906
|
+
function providerIdFromJudge(judge) {
|
|
2907
|
+
if (judge.providerId) {
|
|
2908
|
+
const normalized = normalizeLegacyProviderId(judge.providerId);
|
|
2909
|
+
if (normalized) {
|
|
2910
|
+
return normalized;
|
|
2911
|
+
}
|
|
1951
2912
|
}
|
|
1952
|
-
|
|
1953
|
-
return normalized === ".git" || normalized.startsWith(".git/");
|
|
2913
|
+
return inferProviderIdFromConfig(judge);
|
|
1954
2914
|
}
|
|
1955
|
-
function
|
|
1956
|
-
|
|
1957
|
-
|
|
2915
|
+
function createCliJudgeForProvider(providerId, judgeConfig, config) {
|
|
2916
|
+
const { resolved } = resolveJudgeModel(judgeConfig);
|
|
2917
|
+
const options = {
|
|
2918
|
+
modelRequested: judgeConfig.model,
|
|
2919
|
+
modelResolved: resolved,
|
|
2920
|
+
timeoutMs: judgeConfig.timeoutMs,
|
|
2921
|
+
sensitivePaths: config.classifier.sensitivePaths,
|
|
2922
|
+
scrubOptions: scrubOptionsFromConfig(config)
|
|
2923
|
+
};
|
|
2924
|
+
if (providerId === "codex") {
|
|
2925
|
+
return createCodexCliJudge(options);
|
|
1958
2926
|
}
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
if (matchesSensitivePath(checkPath.replaceAll("\\", "/"), sensitivePaths)) {
|
|
1962
|
-
return true;
|
|
2927
|
+
if (providerId === "cursor") {
|
|
2928
|
+
return createCursorCliJudge(options);
|
|
1963
2929
|
}
|
|
1964
|
-
return
|
|
2930
|
+
return createClaudeCliJudge(options);
|
|
1965
2931
|
}
|
|
1966
|
-
function
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
return {
|
|
1970
|
-
location: "unknown",
|
|
1971
|
-
isHighStakes: false,
|
|
1972
|
-
signals: ["missing_trusted_cwd"]
|
|
1973
|
-
};
|
|
2932
|
+
function createJudgeFromConfig(config, options = {}) {
|
|
2933
|
+
if (process.env.BELAY_DETERMINISTIC_JUDGE === "1") {
|
|
2934
|
+
return createDeterministicJudgeStub();
|
|
1974
2935
|
}
|
|
1975
|
-
const
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
2936
|
+
const judgeConfig = config.judge;
|
|
2937
|
+
if (judgeConfig.providerId && isRemovedProviderId(String(judgeConfig.providerId))) {
|
|
2938
|
+
const { resolved: resolved2 } = resolveJudgeModel(judgeConfig);
|
|
2939
|
+
return createFailClosedJudge({
|
|
2940
|
+
reason: "judge_provider_removed",
|
|
2941
|
+
fallbackReason: "provider_migration_required",
|
|
2942
|
+
modelRequested: judgeConfig.model,
|
|
2943
|
+
modelResolved: resolved2
|
|
2944
|
+
});
|
|
2945
|
+
}
|
|
2946
|
+
const provider = normalizeJudgeProvider(judgeConfig.provider);
|
|
2947
|
+
const providerId = providerIdFromJudge(judgeConfig);
|
|
2948
|
+
const catalogSpec = getJudgeProviderSpec(providerId);
|
|
2949
|
+
const { resolved } = resolveJudgeModel(judgeConfig);
|
|
2950
|
+
const runtime = detectJudgeRuntimeCapabilities(providerId);
|
|
2951
|
+
if (provider === "openai-compatible") {
|
|
2952
|
+
const endpoint = judgeConfig.endpoint?.trim();
|
|
2953
|
+
if (!endpoint && runtime.cliTransport) {
|
|
2954
|
+
if (providerId === "codex" || providerId === "cursor") {
|
|
2955
|
+
return createCliJudgeForProvider(providerId, judgeConfig, config);
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
if (!endpoint) {
|
|
2959
|
+
return createFailClosedJudge({
|
|
2960
|
+
reason: "openai_compatible_endpoint_missing",
|
|
2961
|
+
fallbackReason: "missing_endpoint",
|
|
2962
|
+
modelRequested: judgeConfig.model,
|
|
2963
|
+
modelResolved: resolved
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
if (isCloudJudgeConfig(judgeConfig) && !hasValidCloudConsent(judgeConfig)) {
|
|
2967
|
+
return createFailClosedJudge({
|
|
2968
|
+
reason: "openai_compatible_consent_missing",
|
|
2969
|
+
fallbackReason: "cloud_consent_missing",
|
|
2970
|
+
modelRequested: judgeConfig.model,
|
|
2971
|
+
modelResolved: resolved
|
|
2972
|
+
});
|
|
1989
2973
|
}
|
|
2974
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
2975
|
+
const repoLocalStateDir = repoLocalStateDirFor(repoRoot, config);
|
|
2976
|
+
return createOpenAiCompatibleJudge({
|
|
2977
|
+
endpoint,
|
|
2978
|
+
modelRequested: judgeConfig.model,
|
|
2979
|
+
modelResolved: resolved,
|
|
2980
|
+
timeoutMs: judgeConfig.timeoutMs,
|
|
2981
|
+
sensitivePaths: config.classifier.sensitivePaths,
|
|
2982
|
+
scrubOptions: scrubOptionsFromConfig(config),
|
|
2983
|
+
resolveApiKey: async () => resolveJudgeCredential({
|
|
2984
|
+
judge: judgeConfig,
|
|
2985
|
+
catalogSpec: catalogSpec ?? void 0,
|
|
2986
|
+
repoRoot,
|
|
2987
|
+
repoLocalStateDir,
|
|
2988
|
+
config
|
|
2989
|
+
})
|
|
2990
|
+
});
|
|
1990
2991
|
}
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2992
|
+
if (provider === "ollama") {
|
|
2993
|
+
return createOllamaJudge({
|
|
2994
|
+
model: judgeConfig.model,
|
|
2995
|
+
baseUrl: judgeConfig.endpoint ?? "http://127.0.0.1:11434",
|
|
2996
|
+
timeoutMs: judgeConfig.timeoutMs,
|
|
2997
|
+
keepAlive: judgeConfig.keepAlive
|
|
2998
|
+
});
|
|
1998
2999
|
}
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
3000
|
+
if (provider === "anthropic") {
|
|
3001
|
+
if (runtime.cliTransport) {
|
|
3002
|
+
return createClaudeCliJudge({
|
|
3003
|
+
modelRequested: judgeConfig.model,
|
|
3004
|
+
modelResolved: resolved,
|
|
3005
|
+
timeoutMs: judgeConfig.timeoutMs,
|
|
3006
|
+
sensitivePaths: config.classifier.sensitivePaths,
|
|
3007
|
+
scrubOptions: scrubOptionsFromConfig(config)
|
|
3008
|
+
});
|
|
3009
|
+
}
|
|
3010
|
+
return createFailClosedJudge({
|
|
3011
|
+
reason: "anthropic_not_implemented",
|
|
3012
|
+
fallbackReason: "anthropic_runtime_unavailable",
|
|
3013
|
+
modelRequested: judgeConfig.model,
|
|
3014
|
+
modelResolved: resolved
|
|
3015
|
+
});
|
|
3016
|
+
}
|
|
3017
|
+
return createFailClosedJudge({
|
|
3018
|
+
reason: "unsupported_judge_provider",
|
|
3019
|
+
fallbackReason: "unsupported_provider",
|
|
3020
|
+
modelRequested: judgeConfig.model,
|
|
3021
|
+
modelResolved: resolved
|
|
3022
|
+
});
|
|
2003
3023
|
}
|
|
2004
3024
|
|
|
2005
3025
|
// src/core/verdict/egress-classify.ts
|
|
@@ -2220,7 +3240,7 @@ function verdictFingerprint(cwdRelative2, commandRedacted) {
|
|
|
2220
3240
|
|
|
2221
3241
|
// src/core/verdict/launcher-resolve.ts
|
|
2222
3242
|
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
|
|
2223
|
-
import
|
|
3243
|
+
import path14 from "node:path";
|
|
2224
3244
|
var MAX_RESOLVE_DEPTH = 8;
|
|
2225
3245
|
var PNPM_BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
|
|
2226
3246
|
"add",
|
|
@@ -2267,7 +3287,7 @@ var PNPM_EXEC_LIKE_HEADS = /* @__PURE__ */ new Set([
|
|
|
2267
3287
|
"node"
|
|
2268
3288
|
]);
|
|
2269
3289
|
function readPackageJson(dir) {
|
|
2270
|
-
const packagePath =
|
|
3290
|
+
const packagePath = path14.join(dir, "package.json");
|
|
2271
3291
|
if (!existsSync4(packagePath)) {
|
|
2272
3292
|
return null;
|
|
2273
3293
|
}
|
|
@@ -2278,17 +3298,17 @@ function readPackageJson(dir) {
|
|
|
2278
3298
|
}
|
|
2279
3299
|
}
|
|
2280
3300
|
function findPackageJson(startDir, stopDir) {
|
|
2281
|
-
let current =
|
|
2282
|
-
const stop =
|
|
3301
|
+
let current = path14.resolve(startDir);
|
|
3302
|
+
const stop = path14.resolve(stopDir);
|
|
2283
3303
|
while (true) {
|
|
2284
|
-
const packagePath =
|
|
3304
|
+
const packagePath = path14.join(current, "package.json");
|
|
2285
3305
|
if (existsSync4(packagePath)) {
|
|
2286
3306
|
return packagePath;
|
|
2287
3307
|
}
|
|
2288
|
-
if (current === stop || current ===
|
|
3308
|
+
if (current === stop || current === path14.dirname(current)) {
|
|
2289
3309
|
return existsSync4(packagePath) ? packagePath : null;
|
|
2290
3310
|
}
|
|
2291
|
-
const parent =
|
|
3311
|
+
const parent = path14.dirname(current);
|
|
2292
3312
|
if (!parent.startsWith(stop) && parent !== current) {
|
|
2293
3313
|
}
|
|
2294
3314
|
if (parent === current) {
|
|
@@ -2345,7 +3365,7 @@ function resolveNpmRecipe(cwd, repoRoot, scriptName, extraArgs) {
|
|
|
2345
3365
|
}
|
|
2346
3366
|
return { recipes: [], opaque: true, reason: "package_json_missing" };
|
|
2347
3367
|
}
|
|
2348
|
-
const pkg = readPackageJson(
|
|
3368
|
+
const pkg = readPackageJson(path14.dirname(packagePath));
|
|
2349
3369
|
const scripts = pkg?.scripts;
|
|
2350
3370
|
if (!scripts || typeof scripts !== "object") {
|
|
2351
3371
|
return { recipes: [], opaque: true, reason: "package_scripts_missing" };
|
|
@@ -2410,20 +3430,20 @@ function parseMakefileRecipes(makefilePath) {
|
|
|
2410
3430
|
function resolveMakeRecipe(cwd, repoRoot, target) {
|
|
2411
3431
|
const candidates = ["Makefile", "makefile", "GNUmakefile"];
|
|
2412
3432
|
let makefilePath = null;
|
|
2413
|
-
let searchDir =
|
|
2414
|
-
const stop =
|
|
3433
|
+
let searchDir = path14.resolve(cwd);
|
|
3434
|
+
const stop = path14.resolve(repoRoot);
|
|
2415
3435
|
while (true) {
|
|
2416
3436
|
for (const name of candidates) {
|
|
2417
|
-
const candidate =
|
|
3437
|
+
const candidate = path14.join(searchDir, name);
|
|
2418
3438
|
if (existsSync4(candidate)) {
|
|
2419
3439
|
makefilePath = candidate;
|
|
2420
3440
|
break;
|
|
2421
3441
|
}
|
|
2422
3442
|
}
|
|
2423
|
-
if (makefilePath || searchDir === stop || searchDir ===
|
|
3443
|
+
if (makefilePath || searchDir === stop || searchDir === path14.dirname(searchDir)) {
|
|
2424
3444
|
break;
|
|
2425
3445
|
}
|
|
2426
|
-
searchDir =
|
|
3446
|
+
searchDir = path14.dirname(searchDir);
|
|
2427
3447
|
}
|
|
2428
3448
|
if (!makefilePath) {
|
|
2429
3449
|
return { recipes: [], opaque: true, reason: "unknown_local_effect" };
|
|
@@ -2527,7 +3547,7 @@ function askFromCustomExternal(opacity) {
|
|
|
2527
3547
|
}
|
|
2528
3548
|
|
|
2529
3549
|
// src/core/verdict/parser.ts
|
|
2530
|
-
import
|
|
3550
|
+
import path15 from "node:path";
|
|
2531
3551
|
|
|
2532
3552
|
// src/core/shell-substitution.ts
|
|
2533
3553
|
function findCommandSubstitutions(command) {
|
|
@@ -2777,7 +3797,7 @@ var INTERPRETER_SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
2777
3797
|
".sh"
|
|
2778
3798
|
]);
|
|
2779
3799
|
function normalizeHead(token) {
|
|
2780
|
-
const base =
|
|
3800
|
+
const base = path15.basename(token);
|
|
2781
3801
|
if (base && base !== "." && base !== "..") {
|
|
2782
3802
|
return base;
|
|
2783
3803
|
}
|
|
@@ -2882,7 +3902,7 @@ function isBareInterpreter(tokens) {
|
|
|
2882
3902
|
return false;
|
|
2883
3903
|
}
|
|
2884
3904
|
const scriptArg = args.find((token) => !token.startsWith("-"));
|
|
2885
|
-
if (scriptArg && INTERPRETER_SCRIPT_EXTENSIONS.has(
|
|
3905
|
+
if (scriptArg && INTERPRETER_SCRIPT_EXTENSIONS.has(path15.extname(scriptArg))) {
|
|
2886
3906
|
return false;
|
|
2887
3907
|
}
|
|
2888
3908
|
if (scriptArg) {
|
|
@@ -3181,13 +4201,36 @@ function tier0ExternalMatch(key, head, tokens) {
|
|
|
3181
4201
|
}
|
|
3182
4202
|
return false;
|
|
3183
4203
|
}
|
|
4204
|
+
function isBelayJudgeSelfCommand(tokens) {
|
|
4205
|
+
return tokens[0] === "belay" && tokens[1] === "judge" && (tokens[2] === "use" || tokens[2] === "status" || tokens[2] === "list" || tokens[2] === "test" || tokens[2] === "consent");
|
|
4206
|
+
}
|
|
4207
|
+
function isBelayConfigSelfCommand(tokens) {
|
|
4208
|
+
if (tokens[0] !== "belay" || tokens[1] !== "config") {
|
|
4209
|
+
return false;
|
|
4210
|
+
}
|
|
4211
|
+
const sub = tokens[2];
|
|
4212
|
+
if (!sub) {
|
|
4213
|
+
return true;
|
|
4214
|
+
}
|
|
4215
|
+
if (sub === "list" || sub === "get" || sub === "judge") {
|
|
4216
|
+
return true;
|
|
4217
|
+
}
|
|
4218
|
+
if (sub === "credential") {
|
|
4219
|
+
return true;
|
|
4220
|
+
}
|
|
4221
|
+
if (sub === "set" || sub === "unset") {
|
|
4222
|
+
const pathKey = tokens[3] ?? "";
|
|
4223
|
+
return pathKey.startsWith("judge.");
|
|
4224
|
+
}
|
|
4225
|
+
return false;
|
|
4226
|
+
}
|
|
3184
4227
|
function isBelaySelfCommand(tokens) {
|
|
3185
4228
|
const head = tokens[0] ?? "";
|
|
3186
4229
|
const subcommand = tokens[1] ?? "";
|
|
3187
|
-
return head === "belay" && BELAY_SELF_COMMANDS.has(subcommand);
|
|
4230
|
+
return head === "belay" && (BELAY_SELF_COMMANDS.has(subcommand) || isBelayJudgeSelfCommand(tokens) || isBelayConfigSelfCommand(tokens));
|
|
3188
4231
|
}
|
|
3189
4232
|
function tier0HighStakesRm(tokens, context) {
|
|
3190
|
-
const head = tokens[0] ?? "";
|
|
4233
|
+
const head = (tokens[0] ?? "").split("/").pop() ?? "";
|
|
3191
4234
|
if (head !== "rm") {
|
|
3192
4235
|
return null;
|
|
3193
4236
|
}
|
|
@@ -3210,34 +4253,44 @@ function tier0HighStakesRm(tokens, context) {
|
|
|
3210
4253
|
signals: ["missing_trusted_cwd", ...analysis.signals]
|
|
3211
4254
|
});
|
|
3212
4255
|
}
|
|
3213
|
-
if (analysis.isHighStakes) {
|
|
3214
|
-
return askVerdict({
|
|
3215
|
-
location: analysis.location,
|
|
3216
|
-
opacity: "transparent",
|
|
3217
|
-
effect: "local_mutation",
|
|
3218
|
-
confidence: "deterministic",
|
|
3219
|
-
reason: "high_stakes_path",
|
|
3220
|
-
signals: ["high_stakes_path", ...analysis.signals]
|
|
3221
|
-
});
|
|
3222
|
-
}
|
|
3223
4256
|
for (const target of targets) {
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
4257
|
+
const normalized = target.replace(/^['"]|['"]$/g, "");
|
|
4258
|
+
if (normalized === "~" || normalized === "$HOME" || normalized.startsWith("~/")) {
|
|
4259
|
+
return askVerdict({
|
|
4260
|
+
location: "repo_outside",
|
|
4261
|
+
opacity: "transparent",
|
|
4262
|
+
effect: "local_mutation",
|
|
4263
|
+
confidence: "deterministic",
|
|
4264
|
+
reason: "high_stakes_path",
|
|
4265
|
+
signals: ["catastrophic_home_deletion", ...analysis.signals]
|
|
4266
|
+
});
|
|
4267
|
+
}
|
|
4268
|
+
}
|
|
4269
|
+
if (analysis.isHighStakes && analysis.signals.includes("high_stakes_path")) {
|
|
4270
|
+
let destructiveHighStakes = false;
|
|
4271
|
+
for (const target of targets) {
|
|
4272
|
+
const resolved = resolveTrustedPath(target, context.cwd, context.trustedCwd) ?? resolveMutationTarget(target, context.cwd);
|
|
4273
|
+
if (resolved && isDestructiveHighStakesMutation(
|
|
4274
|
+
head,
|
|
4275
|
+
resolved,
|
|
4276
|
+
context.repoRoot,
|
|
4277
|
+
context.sensitivePaths,
|
|
4278
|
+
context.protectedArtifactRoots
|
|
4279
|
+
)) {
|
|
4280
|
+
destructiveHighStakes = true;
|
|
4281
|
+
break;
|
|
3239
4282
|
}
|
|
3240
4283
|
}
|
|
4284
|
+
if (destructiveHighStakes) {
|
|
4285
|
+
return askVerdict({
|
|
4286
|
+
location: analysis.location,
|
|
4287
|
+
opacity: "transparent",
|
|
4288
|
+
effect: "local_mutation",
|
|
4289
|
+
confidence: "deterministic",
|
|
4290
|
+
reason: "high_stakes_path",
|
|
4291
|
+
signals: ["high_stakes_path", ...analysis.signals]
|
|
4292
|
+
});
|
|
4293
|
+
}
|
|
3241
4294
|
}
|
|
3242
4295
|
return null;
|
|
3243
4296
|
}
|
|
@@ -3436,6 +4489,9 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3436
4489
|
effect = "local_mutation";
|
|
3437
4490
|
}
|
|
3438
4491
|
const pathArgs = extractPathArgs(peeled);
|
|
4492
|
+
if (extractRedirectTargets(peeled).length > 0 && effect === "read_only") {
|
|
4493
|
+
effect = "local_mutation";
|
|
4494
|
+
}
|
|
3439
4495
|
const pathAnalysis = analyzePathTargets({
|
|
3440
4496
|
targets: pathArgs,
|
|
3441
4497
|
cwd: context.cwd,
|
|
@@ -3466,15 +4522,46 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3466
4522
|
});
|
|
3467
4523
|
}
|
|
3468
4524
|
}
|
|
3469
|
-
if (
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
4525
|
+
if ((effect === "local_mutation" || effect === "unknown") && context.protectedArtifactRoots && context.protectedArtifactRoots.length > 0) {
|
|
4526
|
+
for (const target of pathArgs) {
|
|
4527
|
+
const resolved = resolveTrustedPath(target, context.cwd, context.trustedCwd) ?? resolveMutationTarget(target, context.cwd);
|
|
4528
|
+
if (resolved && touchesProtectedRoot(resolved, context.protectedArtifactRoots)) {
|
|
4529
|
+
return askVerdict({
|
|
4530
|
+
location: pathAnalysis.location,
|
|
4531
|
+
opacity: "transparent",
|
|
4532
|
+
effect: "local_mutation",
|
|
4533
|
+
confidence: "deterministic",
|
|
4534
|
+
reason: "high_stakes_path",
|
|
4535
|
+
signals: ["high_stakes_path", "control_plane_path"]
|
|
4536
|
+
});
|
|
4537
|
+
}
|
|
4538
|
+
}
|
|
4539
|
+
}
|
|
4540
|
+
if (effect === "local_mutation" || effect === "unknown") {
|
|
4541
|
+
let destructiveHighStakes = false;
|
|
4542
|
+
for (const target of pathArgs) {
|
|
4543
|
+
const resolved = resolveTrustedPath(target, context.cwd, context.trustedCwd) ?? resolveMutationTarget(target, context.cwd);
|
|
4544
|
+
if (resolved && isDestructiveHighStakesMutation(
|
|
4545
|
+
segment.head,
|
|
4546
|
+
resolved,
|
|
4547
|
+
context.repoRoot,
|
|
4548
|
+
context.sensitivePaths,
|
|
4549
|
+
context.protectedArtifactRoots
|
|
4550
|
+
)) {
|
|
4551
|
+
destructiveHighStakes = true;
|
|
4552
|
+
break;
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
if (destructiveHighStakes) {
|
|
4556
|
+
return askVerdict({
|
|
4557
|
+
location: pathAnalysis.location,
|
|
4558
|
+
opacity: "transparent",
|
|
4559
|
+
effect: "local_mutation",
|
|
4560
|
+
confidence: "deterministic",
|
|
4561
|
+
reason: "high_stakes_path",
|
|
4562
|
+
signals: pathAnalysis.signals
|
|
4563
|
+
});
|
|
4564
|
+
}
|
|
3478
4565
|
}
|
|
3479
4566
|
if (segment.head === "find" && isFindDangerous(peeled)) {
|
|
3480
4567
|
return askVerdict({
|
|
@@ -3486,15 +4573,14 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3486
4573
|
signals: ["find_dangerous_action"]
|
|
3487
4574
|
});
|
|
3488
4575
|
}
|
|
3489
|
-
if (pathAnalysis.location === "repo_outside" || pathAnalysis.location === "mixed") {
|
|
3490
|
-
|
|
3491
|
-
return askVerdict({
|
|
4576
|
+
if (effect === "read_only" && (pathAnalysis.location === "repo_outside" || pathAnalysis.location === "mixed")) {
|
|
4577
|
+
return allowVerdict({
|
|
3492
4578
|
location: pathAnalysis.location,
|
|
3493
|
-
opacity
|
|
3494
|
-
effect:
|
|
4579
|
+
opacity,
|
|
4580
|
+
effect: "read_only",
|
|
3495
4581
|
confidence: "deterministic",
|
|
3496
|
-
reason: "
|
|
3497
|
-
signals: ["
|
|
4582
|
+
reason: "read_only",
|
|
4583
|
+
signals: ["read_only", ...pathAnalysis.signals]
|
|
3498
4584
|
});
|
|
3499
4585
|
}
|
|
3500
4586
|
if (pathAnalysis.location === "unknown" && pathArgs.length > 0 && LOCAL_MUTATION_KEYS.has(segment.head)) {
|
|
@@ -3507,7 +4593,25 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3507
4593
|
signals: ["unknown_location_mutation"]
|
|
3508
4594
|
});
|
|
3509
4595
|
}
|
|
3510
|
-
const
|
|
4596
|
+
const outsideMutation = pathAnalysis.location === "repo_outside" || pathAnalysis.location === "mixed";
|
|
4597
|
+
const mutationPrescan = (effect === "local_mutation" || effect === "unknown") && pathArgs.length > 0 ? mutationPrescanRequiresAsk({
|
|
4598
|
+
targets: pathArgs,
|
|
4599
|
+
cwd: context.cwd,
|
|
4600
|
+
repoRoot: context.repoRoot,
|
|
4601
|
+
trustedCwd: context.trustedCwd,
|
|
4602
|
+
sensitivePaths: context.sensitivePaths
|
|
4603
|
+
}) : null;
|
|
4604
|
+
if (mutationPrescan) {
|
|
4605
|
+
return askVerdict({
|
|
4606
|
+
location: pathAnalysis.location === "unknown" ? "unknown" : pathAnalysis.location === "repo_outside" || pathAnalysis.location === "mixed" ? pathAnalysis.location : "repo_local",
|
|
4607
|
+
opacity,
|
|
4608
|
+
effect: "local_mutation",
|
|
4609
|
+
confidence: "deterministic",
|
|
4610
|
+
reason: "tier1_catastrophic",
|
|
4611
|
+
signals: ["tier1_catastrophic", mutationPrescan.reason]
|
|
4612
|
+
});
|
|
4613
|
+
}
|
|
4614
|
+
const needsTier1 = effect === "unknown" || TIER0_EXTERNAL_HEADS.has(segment.head) || egressClass === "ambiguous" || outsideMutation && effect !== "read_only";
|
|
3511
4615
|
let tier1Trace;
|
|
3512
4616
|
if (needsTier1) {
|
|
3513
4617
|
const tier1Text = recursiveScript ?? command;
|
|
@@ -3519,9 +4623,9 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3519
4623
|
tier1Trace = context.judge.lastTrace;
|
|
3520
4624
|
if (tier1RequiresAsk(tier1)) {
|
|
3521
4625
|
return askVerdict({
|
|
3522
|
-
location: pathAnalysis.location === "unknown" ? "unknown" : "repo_local",
|
|
4626
|
+
location: pathAnalysis.location === "unknown" ? "unknown" : pathAnalysis.location === "repo_outside" || pathAnalysis.location === "mixed" ? pathAnalysis.location : "repo_local",
|
|
3523
4627
|
opacity,
|
|
3524
|
-
effect: tier1.
|
|
4628
|
+
effect: !tier1.local_recoverable ? "remote_mutation" : effect,
|
|
3525
4629
|
confidence: "llm",
|
|
3526
4630
|
reason: "tier1_catastrophic",
|
|
3527
4631
|
signals: ["tier1_catastrophic", tier1.reason],
|
|
@@ -3529,6 +4633,19 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3529
4633
|
});
|
|
3530
4634
|
}
|
|
3531
4635
|
}
|
|
4636
|
+
if (outsideMutation && effect !== "read_only") {
|
|
4637
|
+
return withJudgeTrace(
|
|
4638
|
+
allowVerdict({
|
|
4639
|
+
location: pathAnalysis.location,
|
|
4640
|
+
opacity,
|
|
4641
|
+
effect: "local_mutation",
|
|
4642
|
+
confidence: tier1Trace ? "llm" : "deterministic",
|
|
4643
|
+
reason: "repo_outside_local_mutation",
|
|
4644
|
+
signals: ["repo_outside_local_mutation", ...pathAnalysis.signals]
|
|
4645
|
+
}),
|
|
4646
|
+
tier1Trace
|
|
4647
|
+
);
|
|
4648
|
+
}
|
|
3532
4649
|
if (pathAnalysis.location === "repo_local" && (effect === "read_only" || effect === "local_mutation") && opacity !== "opaque") {
|
|
3533
4650
|
return withJudgeTrace(
|
|
3534
4651
|
allowVerdict({
|
|
@@ -3648,6 +4765,12 @@ async function verdict(command, context) {
|
|
|
3648
4765
|
}
|
|
3649
4766
|
|
|
3650
4767
|
// src/core/verdict/adapter.ts
|
|
4768
|
+
function resolveClassifierTrustedCwd(cwd, options, explicit) {
|
|
4769
|
+
if (explicit !== void 0) {
|
|
4770
|
+
return explicit;
|
|
4771
|
+
}
|
|
4772
|
+
return options?.trustedCwd ?? Boolean(cwd);
|
|
4773
|
+
}
|
|
3651
4774
|
function buildVerdictContext(params) {
|
|
3652
4775
|
const protectedArtifactRoots2 = [
|
|
3653
4776
|
...params.options?.protectedArtifactRoots ?? [],
|
|
@@ -3656,7 +4779,7 @@ function buildVerdictContext(params) {
|
|
|
3656
4779
|
return {
|
|
3657
4780
|
cwd: params.cwd,
|
|
3658
4781
|
repoRoot: params.repoRoot,
|
|
3659
|
-
trustedCwd: params.
|
|
4782
|
+
trustedCwd: resolveClassifierTrustedCwd(params.cwd, params.options, params.trustedCwd),
|
|
3660
4783
|
sensitivePaths: params.options?.sensitivePaths ?? params.config.classifier.sensitivePaths,
|
|
3661
4784
|
protectedArtifactRoots: protectedArtifactRoots2.length > 0 ? [...new Set(protectedArtifactRoots2)] : void 0,
|
|
3662
4785
|
customAllowCommands: params.options?.customAllowCommands ?? params.config.overrides.allow,
|
|
@@ -3788,7 +4911,7 @@ function extractPatch(payload) {
|
|
|
3788
4911
|
}
|
|
3789
4912
|
return null;
|
|
3790
4913
|
}
|
|
3791
|
-
function
|
|
4914
|
+
function applyPatchTargets2(patch) {
|
|
3792
4915
|
const targets = [];
|
|
3793
4916
|
for (const line of patch.split("\n")) {
|
|
3794
4917
|
const match = line.match(/^\*\*\* (Add|Delete|Update) File: (.+)$/);
|
|
@@ -3806,6 +4929,70 @@ function applyPatchTargets(patch) {
|
|
|
3806
4929
|
function normalizedToolName(toolName) {
|
|
3807
4930
|
return toolName.trim().toLowerCase();
|
|
3808
4931
|
}
|
|
4932
|
+
function resolveFileTier1Judge(config, options, repoRoot) {
|
|
4933
|
+
return options.tier1Judge ?? createJudgeFromConfig(config, { repoRoot });
|
|
4934
|
+
}
|
|
4935
|
+
async function classifyFileMutationWithTier1(params) {
|
|
4936
|
+
const judge = resolveFileTier1Judge(params.config, params.options, params.repoRoot);
|
|
4937
|
+
const trustedCwd = resolveClassifierTrustedCwd(params.cwd, params.options);
|
|
4938
|
+
const prescan = mutationPrescanRequiresAsk({
|
|
4939
|
+
targets: [params.filePath],
|
|
4940
|
+
cwd: params.cwd,
|
|
4941
|
+
repoRoot: params.repoRoot,
|
|
4942
|
+
trustedCwd,
|
|
4943
|
+
sensitivePaths: params.options.sensitivePaths ?? params.config.classifier.sensitivePaths
|
|
4944
|
+
});
|
|
4945
|
+
if (prescan) {
|
|
4946
|
+
return {
|
|
4947
|
+
verdict: "deny_pending_approval",
|
|
4948
|
+
reason: "tier1_catastrophic",
|
|
4949
|
+
summary: params.filePath,
|
|
4950
|
+
fingerprint: toolFingerprint(params.toolName, { path: params.filePath }, params.repoRoot),
|
|
4951
|
+
assessment: {
|
|
4952
|
+
reversibility: "irreversible",
|
|
4953
|
+
external: params.locationLabel === "outside_repo",
|
|
4954
|
+
blastRadius: params.locationLabel === "outside_repo" ? "outside the repository" : "sensitive repository file",
|
|
4955
|
+
confidence: 0.95,
|
|
4956
|
+
signals: [...params.signals, "tier1_catastrophic", prescan.reason]
|
|
4957
|
+
}
|
|
4958
|
+
};
|
|
4959
|
+
}
|
|
4960
|
+
const tier1Text = `file_mutation: ${params.toolKind} path=${params.resolvedPath} location=${params.locationLabel}`;
|
|
4961
|
+
const tier1 = await judge.evaluate({
|
|
4962
|
+
text: tier1Text,
|
|
4963
|
+
context: { cwd: params.cwd, repoRoot: params.repoRoot }
|
|
4964
|
+
});
|
|
4965
|
+
const fingerprint = toolFingerprint(params.toolName, { path: params.filePath }, params.repoRoot);
|
|
4966
|
+
if (tier1RequiresAsk(tier1)) {
|
|
4967
|
+
return {
|
|
4968
|
+
verdict: "deny_pending_approval",
|
|
4969
|
+
reason: "tier1_catastrophic",
|
|
4970
|
+
summary: params.filePath,
|
|
4971
|
+
fingerprint,
|
|
4972
|
+
assessment: {
|
|
4973
|
+
reversibility: "irreversible",
|
|
4974
|
+
external: params.locationLabel === "outside_repo",
|
|
4975
|
+
blastRadius: params.locationLabel === "outside_repo" ? "outside the repository" : "sensitive repository file",
|
|
4976
|
+
confidence: 0.82,
|
|
4977
|
+
signals: [...params.signals, "tier1_catastrophic", tier1.reason]
|
|
4978
|
+
}
|
|
4979
|
+
};
|
|
4980
|
+
}
|
|
4981
|
+
const reason = params.isDelete ? "file_delete" : "file_mutation";
|
|
4982
|
+
return {
|
|
4983
|
+
verdict: "allow_flagged",
|
|
4984
|
+
reason,
|
|
4985
|
+
summary: params.filePath,
|
|
4986
|
+
fingerprint,
|
|
4987
|
+
assessment: {
|
|
4988
|
+
reversibility: "recoverable_with_cost",
|
|
4989
|
+
external: params.locationLabel === "outside_repo",
|
|
4990
|
+
blastRadius: params.locationLabel === "outside_repo" ? "outside the repository" : "this repository",
|
|
4991
|
+
confidence: 0.72,
|
|
4992
|
+
signals: [...params.signals, "tier1_restorable", tier1.reason]
|
|
4993
|
+
}
|
|
4994
|
+
};
|
|
4995
|
+
}
|
|
3809
4996
|
async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
|
|
3810
4997
|
const toolName = String(payload.tool_name ?? "");
|
|
3811
4998
|
const toolKind = normalizedToolName(toolName);
|
|
@@ -3901,7 +5088,7 @@ async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
|
|
|
3901
5088
|
};
|
|
3902
5089
|
}
|
|
3903
5090
|
const signals = [];
|
|
3904
|
-
const resolvedPath =
|
|
5091
|
+
const resolvedPath = path16.isAbsolute(filePath) ? filePath : path16.resolve(cwd, filePath);
|
|
3905
5092
|
const hitsProtectedRoot = protectedRoots.some((root) => pathWithinRoot(root, resolvedPath));
|
|
3906
5093
|
if (hitsProtectedRoot) {
|
|
3907
5094
|
signals.push("control_plane_path");
|
|
@@ -3922,35 +5109,35 @@ async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
|
|
|
3922
5109
|
const relativePath = relativeWithinRepo(repoRoot, resolvedPath);
|
|
3923
5110
|
if (relativePath === null) {
|
|
3924
5111
|
signals.push("outside_repo_path");
|
|
3925
|
-
return {
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
};
|
|
5112
|
+
return classifyFileMutationWithTier1({
|
|
5113
|
+
toolName,
|
|
5114
|
+
toolKind,
|
|
5115
|
+
filePath,
|
|
5116
|
+
resolvedPath,
|
|
5117
|
+
repoRoot,
|
|
5118
|
+
cwd,
|
|
5119
|
+
config,
|
|
5120
|
+
options,
|
|
5121
|
+
signals,
|
|
5122
|
+
isDelete: FILE_DELETE_TOOL_NAMES.has(toolKind),
|
|
5123
|
+
locationLabel: "outside_repo"
|
|
5124
|
+
});
|
|
3938
5125
|
}
|
|
3939
5126
|
if (matchesSensitivePath(relativePath, sensitivePaths)) {
|
|
3940
5127
|
signals.push("sensitive_path");
|
|
3941
|
-
return {
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
};
|
|
5128
|
+
return classifyFileMutationWithTier1({
|
|
5129
|
+
toolName,
|
|
5130
|
+
toolKind,
|
|
5131
|
+
filePath,
|
|
5132
|
+
resolvedPath,
|
|
5133
|
+
repoRoot,
|
|
5134
|
+
cwd,
|
|
5135
|
+
config,
|
|
5136
|
+
options,
|
|
5137
|
+
signals,
|
|
5138
|
+
isDelete: FILE_DELETE_TOOL_NAMES.has(toolKind),
|
|
5139
|
+
locationLabel: "sensitive_path"
|
|
5140
|
+
});
|
|
3954
5141
|
}
|
|
3955
5142
|
if (FILE_DELETE_TOOL_NAMES.has(toolKind)) {
|
|
3956
5143
|
signals.push("file_delete");
|
|
@@ -3985,7 +5172,7 @@ async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
|
|
|
3985
5172
|
}
|
|
3986
5173
|
if (APPLY_PATCH_TOOL_NAMES.has(toolKind)) {
|
|
3987
5174
|
const patch = extractPatch(payload);
|
|
3988
|
-
const targets = patch ?
|
|
5175
|
+
const targets = patch ? applyPatchTargets2(patch) : [];
|
|
3989
5176
|
if (targets.length === 0) {
|
|
3990
5177
|
if (options.unknownLocalEffect === "deny") {
|
|
3991
5178
|
return {
|
|
@@ -4175,10 +5362,56 @@ function normalizeGatedAction(params) {
|
|
|
4175
5362
|
agentAssessment
|
|
4176
5363
|
};
|
|
4177
5364
|
}
|
|
4178
|
-
function
|
|
4179
|
-
if (options.brokerFsScope
|
|
4180
|
-
|
|
4181
|
-
|
|
5365
|
+
function applySandboxFsScopeBoundary(outsideRepoPaths, result, options, hints = {}) {
|
|
5366
|
+
if (!options.brokerFsScope) {
|
|
5367
|
+
return result;
|
|
5368
|
+
}
|
|
5369
|
+
if (outsideRepoPaths.length === 0) {
|
|
5370
|
+
return result;
|
|
5371
|
+
}
|
|
5372
|
+
if (options.fsScopeAllowlist && allPathsAllowlisted(outsideRepoPaths, options.fsScopeAllowlist)) {
|
|
5373
|
+
return result;
|
|
5374
|
+
}
|
|
5375
|
+
if (result.verdict === "deny_pending_approval") {
|
|
5376
|
+
return result;
|
|
5377
|
+
}
|
|
5378
|
+
const redirect = hints.redirect === true || result.reason === "read_only" || result.assessment.signals.includes("read_only") || result.reason === "outside_repo_redirect";
|
|
5379
|
+
return {
|
|
5380
|
+
...result,
|
|
5381
|
+
verdict: "deny_pending_approval",
|
|
5382
|
+
reason: redirect ? "outside_repo_redirect" : "outside_repo_mutation",
|
|
5383
|
+
assessment: {
|
|
5384
|
+
...result.assessment,
|
|
5385
|
+
external: true,
|
|
5386
|
+
reversibility: "irreversible",
|
|
5387
|
+
signals: [...result.assessment.signals, "sandbox_boundary_expected"]
|
|
5388
|
+
}
|
|
5389
|
+
};
|
|
5390
|
+
}
|
|
5391
|
+
function applyFsScopePeripheralPolicy(outsideRepoPaths, result, options) {
|
|
5392
|
+
if (!options.brokerFsScope) {
|
|
5393
|
+
return result;
|
|
5394
|
+
}
|
|
5395
|
+
if (outsideRepoPaths.length === 0) {
|
|
5396
|
+
return result;
|
|
5397
|
+
}
|
|
5398
|
+
if (options.fsScopeAllowlist && allPathsAllowlisted(outsideRepoPaths, options.fsScopeAllowlist)) {
|
|
5399
|
+
if (result.verdict === "deny_pending_approval" && (result.reason === "outside_repo_mutation" || result.reason === "outside_repo_redirect" || result.reason === "repo_outside_mutation" || result.axes?.location === "repo_outside")) {
|
|
5400
|
+
return {
|
|
5401
|
+
...result,
|
|
5402
|
+
verdict: "allow_flagged",
|
|
5403
|
+
reason: "capability_fs_hint",
|
|
5404
|
+
assessment: {
|
|
5405
|
+
...result.assessment,
|
|
5406
|
+
signals: [
|
|
5407
|
+
...result.assessment.signals,
|
|
5408
|
+
"capability_fs_hint",
|
|
5409
|
+
"sandbox_boundary_expected"
|
|
5410
|
+
]
|
|
5411
|
+
}
|
|
5412
|
+
};
|
|
5413
|
+
}
|
|
5414
|
+
if (result.verdict === "allow" || result.verdict === "allow_flagged") {
|
|
4182
5415
|
return {
|
|
4183
5416
|
...result,
|
|
4184
5417
|
verdict: "allow_flagged",
|
|
@@ -4196,6 +5429,38 @@ function applyShellPeripheralPolicy(command, action, result, options) {
|
|
|
4196
5429
|
}
|
|
4197
5430
|
return result;
|
|
4198
5431
|
}
|
|
5432
|
+
function applySandboxOutsideBoundary(command, action, result, options) {
|
|
5433
|
+
const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot);
|
|
5434
|
+
return applySandboxFsScopeBoundary(outsideRepoPaths, result, options, {
|
|
5435
|
+
redirect: command.includes(">")
|
|
5436
|
+
});
|
|
5437
|
+
}
|
|
5438
|
+
function applyShellPeripheralPolicy(command, action, result, options) {
|
|
5439
|
+
const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot);
|
|
5440
|
+
return applyFsScopePeripheralPolicy(outsideRepoPaths, result, options);
|
|
5441
|
+
}
|
|
5442
|
+
function outsideRepoPathsForToolAction(action) {
|
|
5443
|
+
const payload = action.payload ?? {};
|
|
5444
|
+
const paths = new Set(
|
|
5445
|
+
collectOutsideRepoPathsFromToolPayload(payload, action.cwd, action.repoRoot)
|
|
5446
|
+
);
|
|
5447
|
+
const command = shellCommandFromPayload(payload);
|
|
5448
|
+
if (command) {
|
|
5449
|
+
for (const resolved of collectOutsideRepoPaths(command, action.cwd, action.repoRoot)) {
|
|
5450
|
+
paths.add(resolved);
|
|
5451
|
+
}
|
|
5452
|
+
}
|
|
5453
|
+
return [...paths];
|
|
5454
|
+
}
|
|
5455
|
+
function applyToolSandboxPolicies(action, result, options) {
|
|
5456
|
+
const outsideRepoPaths = outsideRepoPathsForToolAction(action);
|
|
5457
|
+
const command = shellCommandFromPayload(action.payload ?? {});
|
|
5458
|
+
let next = applySandboxFsScopeBoundary(outsideRepoPaths, result, options, {
|
|
5459
|
+
redirect: command.includes(">")
|
|
5460
|
+
});
|
|
5461
|
+
next = applyFsScopePeripheralPolicy(outsideRepoPaths, next, options);
|
|
5462
|
+
return next;
|
|
5463
|
+
}
|
|
4199
5464
|
async function classifyGatedAction(action, config, extraOptions = {}) {
|
|
4200
5465
|
const options = { ...classifierOptionsFromConfig(config), ...extraOptions };
|
|
4201
5466
|
if (action.kind === "shell") {
|
|
@@ -4203,26 +5468,47 @@ async function classifyGatedAction(action, config, extraOptions = {}) {
|
|
|
4203
5468
|
if (!command) {
|
|
4204
5469
|
throw new GateNormalizationError("Shell gated action requires a command.");
|
|
4205
5470
|
}
|
|
4206
|
-
let
|
|
4207
|
-
|
|
5471
|
+
let result2 = await classifyShell(command, action.cwd, action.repoRoot, config, options);
|
|
5472
|
+
result2 = applySandboxOutsideBoundary(command, action, result2, options);
|
|
5473
|
+
result2 = applyShellPeripheralPolicy(command, action, result2, options);
|
|
4208
5474
|
if (!action.agentAssessment) {
|
|
4209
|
-
return
|
|
5475
|
+
return result2;
|
|
4210
5476
|
}
|
|
4211
|
-
const
|
|
4212
|
-
if (!
|
|
4213
|
-
return { ...
|
|
5477
|
+
const merged2 = mergeAgentAssessment(result2.assessment, action.agentAssessment);
|
|
5478
|
+
if (!merged2.mismatch) {
|
|
5479
|
+
return { ...result2, assessment: merged2.assessment };
|
|
4214
5480
|
}
|
|
4215
5481
|
return {
|
|
4216
|
-
...
|
|
5482
|
+
...result2,
|
|
4217
5483
|
verdict: "deny_pending_approval",
|
|
4218
5484
|
reason: "agent_assessment_mismatch",
|
|
4219
|
-
assessment:
|
|
5485
|
+
assessment: merged2.assessment
|
|
4220
5486
|
};
|
|
4221
5487
|
}
|
|
4222
5488
|
if (action.kind === "subagent") {
|
|
4223
5489
|
return classifySubagent(action.payload ?? {}, action.repoRoot, options);
|
|
4224
5490
|
}
|
|
4225
|
-
|
|
5491
|
+
let result = await classifyToolUse(
|
|
5492
|
+
action.payload ?? {},
|
|
5493
|
+
action.repoRoot,
|
|
5494
|
+
action.cwd,
|
|
5495
|
+
config,
|
|
5496
|
+
options
|
|
5497
|
+
);
|
|
5498
|
+
result = applyToolSandboxPolicies(action, result, options);
|
|
5499
|
+
if (!action.agentAssessment) {
|
|
5500
|
+
return result;
|
|
5501
|
+
}
|
|
5502
|
+
const merged = mergeAgentAssessment(result.assessment, action.agentAssessment);
|
|
5503
|
+
if (!merged.mismatch) {
|
|
5504
|
+
return { ...result, assessment: merged.assessment };
|
|
5505
|
+
}
|
|
5506
|
+
return {
|
|
5507
|
+
...result,
|
|
5508
|
+
verdict: "deny_pending_approval",
|
|
5509
|
+
reason: "agent_assessment_mismatch",
|
|
5510
|
+
assessment: merged.assessment
|
|
5511
|
+
};
|
|
4226
5512
|
}
|
|
4227
5513
|
async function classifyGatedActionAsync(action, config, extraOptions = {}) {
|
|
4228
5514
|
return classifyGatedAction(action, config, extraOptions);
|
|
@@ -4248,9 +5534,9 @@ function gateEnabledForAction(config, action) {
|
|
|
4248
5534
|
}
|
|
4249
5535
|
|
|
4250
5536
|
// src/core/transactional/diff-evaluator.ts
|
|
4251
|
-
import
|
|
5537
|
+
import path17 from "node:path";
|
|
4252
5538
|
function categorizeChange(change, ctx) {
|
|
4253
|
-
const absolutePath = canonicalPath(
|
|
5539
|
+
const absolutePath = canonicalPath(path17.join(ctx.repoRoot, change.relativePath));
|
|
4254
5540
|
if (!pathWithinRoot(ctx.repoRoot, absolutePath)) {
|
|
4255
5541
|
return "repo_outside";
|
|
4256
5542
|
}
|
|
@@ -4272,15 +5558,6 @@ function observedAssessment(evaluation) {
|
|
|
4272
5558
|
if (evaluation.deletedCount > 0) {
|
|
4273
5559
|
signals.push("observed_deletions");
|
|
4274
5560
|
}
|
|
4275
|
-
if (evaluation.categories.includes("repo_outside") || evaluation.categories.includes("control_plane") || evaluation.categories.includes("sensitive_path")) {
|
|
4276
|
-
return {
|
|
4277
|
-
reversibility: "irreversible",
|
|
4278
|
-
external: evaluation.categories.includes("repo_outside"),
|
|
4279
|
-
blastRadius: evaluation.categories.includes("control_plane") ? "agent-belay control plane" : evaluation.categories.includes("repo_outside") ? "outside the repository" : "sensitive path",
|
|
4280
|
-
confidence: 1,
|
|
4281
|
-
signals
|
|
4282
|
-
};
|
|
4283
|
-
}
|
|
4284
5561
|
if (evaluation.categories.includes("large_deletion")) {
|
|
4285
5562
|
return {
|
|
4286
5563
|
reversibility: "irreversible",
|
|
@@ -4308,7 +5585,7 @@ function evaluateTransactionalDiff(changes, ctx) {
|
|
|
4308
5585
|
categories.add("large_deletion");
|
|
4309
5586
|
}
|
|
4310
5587
|
const categoryList = [...categories];
|
|
4311
|
-
const dangerous = categoryList.includes("
|
|
5588
|
+
const dangerous = categoryList.includes("large_deletion");
|
|
4312
5589
|
const base = {
|
|
4313
5590
|
categories: categoryList,
|
|
4314
5591
|
changes,
|
|
@@ -4379,15 +5656,15 @@ var TRANSACTIONAL_APPROVAL_BYPASS_REASONS = /* @__PURE__ */ new Set([
|
|
|
4379
5656
|
]);
|
|
4380
5657
|
|
|
4381
5658
|
// src/core/transactional/git-worktree.ts
|
|
4382
|
-
import { spawn } from "node:child_process";
|
|
5659
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
4383
5660
|
import { randomUUID } from "node:crypto";
|
|
4384
5661
|
import { existsSync as existsSync5 } from "node:fs";
|
|
4385
|
-
import { copyFile, mkdir as
|
|
5662
|
+
import { copyFile, mkdir as mkdir3, mkdtemp, rm } from "node:fs/promises";
|
|
4386
5663
|
import os from "node:os";
|
|
4387
|
-
import
|
|
5664
|
+
import path18 from "node:path";
|
|
4388
5665
|
function execGit(repoRoot, args) {
|
|
4389
5666
|
return new Promise((resolve, reject) => {
|
|
4390
|
-
const child =
|
|
5667
|
+
const child = spawn2("git", ["-C", repoRoot, ...args], {
|
|
4391
5668
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4392
5669
|
});
|
|
4393
5670
|
const stdout = [];
|
|
@@ -4425,8 +5702,8 @@ async function isDirtyWorktree(repoRoot) {
|
|
|
4425
5702
|
}
|
|
4426
5703
|
}
|
|
4427
5704
|
async function createGitWorktreeSnapshot(repoRoot, stateDir) {
|
|
4428
|
-
const worktreePath =
|
|
4429
|
-
await
|
|
5705
|
+
const worktreePath = path18.join(stateDir, `tx-${randomUUID().replaceAll("-", "")}`);
|
|
5706
|
+
await mkdir3(stateDir, { recursive: true });
|
|
4430
5707
|
await execGit(repoRoot, ["worktree", "add", "--detach", worktreePath, "HEAD"]);
|
|
4431
5708
|
return {
|
|
4432
5709
|
worktreePath,
|
|
@@ -4445,18 +5722,18 @@ async function createGitWorktreeSnapshot(repoRoot, stateDir) {
|
|
|
4445
5722
|
}
|
|
4446
5723
|
function resolveWorktreeCwd(repoRoot, worktreePath, cwd) {
|
|
4447
5724
|
const resolvedCwd = canonicalPath(cwd);
|
|
4448
|
-
const relative =
|
|
4449
|
-
if (relative.startsWith("..") ||
|
|
5725
|
+
const relative = path18.relative(canonicalPath(repoRoot), resolvedCwd);
|
|
5726
|
+
if (relative.startsWith("..") || path18.isAbsolute(relative)) {
|
|
4450
5727
|
return worktreePath;
|
|
4451
5728
|
}
|
|
4452
5729
|
if (relative === "") {
|
|
4453
5730
|
return worktreePath;
|
|
4454
5731
|
}
|
|
4455
|
-
return
|
|
5732
|
+
return path18.join(worktreePath, relative);
|
|
4456
5733
|
}
|
|
4457
5734
|
function runShellCommand(command, cwd, timeoutMs) {
|
|
4458
5735
|
return new Promise((resolve) => {
|
|
4459
|
-
const child =
|
|
5736
|
+
const child = spawn2(command, {
|
|
4460
5737
|
cwd,
|
|
4461
5738
|
shell: true,
|
|
4462
5739
|
stdio: "ignore",
|
|
@@ -4522,7 +5799,7 @@ async function rollbackAppliedChanges(actions) {
|
|
|
4522
5799
|
for (const action of [...actions].reverse()) {
|
|
4523
5800
|
try {
|
|
4524
5801
|
if (action.type === "restore") {
|
|
4525
|
-
await
|
|
5802
|
+
await mkdir3(path18.dirname(action.target), { recursive: true });
|
|
4526
5803
|
await copyFile(action.backupPath, action.target);
|
|
4527
5804
|
} else {
|
|
4528
5805
|
await rm(action.target, { force: true });
|
|
@@ -4532,14 +5809,14 @@ async function rollbackAppliedChanges(actions) {
|
|
|
4532
5809
|
}
|
|
4533
5810
|
}
|
|
4534
5811
|
async function applyWorktreeChanges(worktreePath, repoRoot, changes) {
|
|
4535
|
-
const backupRoot = await mkdtemp(
|
|
5812
|
+
const backupRoot = await mkdtemp(path18.join(os.tmpdir(), "belay-tx-rollback-"));
|
|
4536
5813
|
const rollbackActions = [];
|
|
4537
5814
|
try {
|
|
4538
5815
|
for (const change of changes) {
|
|
4539
|
-
const target =
|
|
5816
|
+
const target = path18.join(repoRoot, change.relativePath);
|
|
4540
5817
|
if (existsSync5(target)) {
|
|
4541
|
-
const backupPath =
|
|
4542
|
-
await
|
|
5818
|
+
const backupPath = path18.join(backupRoot, change.relativePath);
|
|
5819
|
+
await mkdir3(path18.dirname(backupPath), { recursive: true });
|
|
4543
5820
|
await copyFile(target, backupPath);
|
|
4544
5821
|
rollbackActions.push({ type: "restore", target, backupPath });
|
|
4545
5822
|
} else if (change.kind !== "deleted") {
|
|
@@ -4549,8 +5826,8 @@ async function applyWorktreeChanges(worktreePath, repoRoot, changes) {
|
|
|
4549
5826
|
await rm(target, { force: true });
|
|
4550
5827
|
continue;
|
|
4551
5828
|
}
|
|
4552
|
-
const source =
|
|
4553
|
-
await
|
|
5829
|
+
const source = path18.join(worktreePath, change.relativePath);
|
|
5830
|
+
await mkdir3(path18.dirname(target), { recursive: true });
|
|
4554
5831
|
await copyFile(source, target);
|
|
4555
5832
|
}
|
|
4556
5833
|
} catch (error) {
|
|
@@ -4712,7 +5989,7 @@ async function notifyDeny(config, event) {
|
|
|
4712
5989
|
}
|
|
4713
5990
|
|
|
4714
5991
|
// src/adapters/layouts/protected-paths.ts
|
|
4715
|
-
import
|
|
5992
|
+
import path19 from "node:path";
|
|
4716
5993
|
function protectedArtifactRoots(layout, repoRoot, controlPlaneDir) {
|
|
4717
5994
|
const roots = [
|
|
4718
5995
|
layout.configPath(repoRoot),
|
|
@@ -4724,7 +6001,7 @@ function protectedArtifactRoots(layout, repoRoot, controlPlaneDir) {
|
|
|
4724
6001
|
if (controlPlaneDir) {
|
|
4725
6002
|
roots.push(controlPlaneDir);
|
|
4726
6003
|
}
|
|
4727
|
-
return roots.map((entry) =>
|
|
6004
|
+
return roots.map((entry) => path19.resolve(entry));
|
|
4728
6005
|
}
|
|
4729
6006
|
|
|
4730
6007
|
// src/adapters/shared/gate-runtime.ts
|
|
@@ -4734,7 +6011,7 @@ var EMPTY_APPROVALS = {
|
|
|
4734
6011
|
};
|
|
4735
6012
|
async function loadJsonFile(filePath, fallback) {
|
|
4736
6013
|
try {
|
|
4737
|
-
const raw = await
|
|
6014
|
+
const raw = await readFile3(filePath, "utf8");
|
|
4738
6015
|
return JSON.parse(raw);
|
|
4739
6016
|
} catch {
|
|
4740
6017
|
return fallback;
|
|
@@ -4746,14 +6023,14 @@ function createDefaultGateRuntimeDeps() {
|
|
|
4746
6023
|
return loadJsonFile(configPath, {});
|
|
4747
6024
|
},
|
|
4748
6025
|
async appendAudit(ctx, event) {
|
|
4749
|
-
const auditPath =
|
|
4750
|
-
await
|
|
6026
|
+
const auditPath = path20.join(ctx.repoRoot, ctx.config.audit.logPath);
|
|
6027
|
+
await mkdir4(path20.dirname(auditPath), { recursive: true });
|
|
4751
6028
|
const record = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), ...event };
|
|
4752
6029
|
if (!ctx.config.audit.includeAssessment) {
|
|
4753
6030
|
delete record.assessment;
|
|
4754
6031
|
}
|
|
4755
6032
|
const scrubbed = scrubValue(record, scrubOptionsFromConfig(ctx.config));
|
|
4756
|
-
await
|
|
6033
|
+
await writeFile3(auditPath, `${JSON.stringify(scrubbed)}
|
|
4757
6034
|
`, {
|
|
4758
6035
|
encoding: "utf8",
|
|
4759
6036
|
flag: "a"
|
|
@@ -4772,8 +6049,8 @@ function createDefaultGateRuntimeDeps() {
|
|
|
4772
6049
|
};
|
|
4773
6050
|
},
|
|
4774
6051
|
async writeApprovals(filePath, state) {
|
|
4775
|
-
await
|
|
4776
|
-
await
|
|
6052
|
+
await mkdir4(path20.dirname(filePath), { recursive: true });
|
|
6053
|
+
await writeFile3(filePath, `${JSON.stringify(compactApprovals(state), null, 2)}
|
|
4777
6054
|
`, "utf8");
|
|
4778
6055
|
}
|
|
4779
6056
|
};
|
|
@@ -4783,7 +6060,7 @@ async function resolveGateConfig(ctx, deps) {
|
|
|
4783
6060
|
let teamConfig = null;
|
|
4784
6061
|
const teamPath = teamConfigPath();
|
|
4785
6062
|
if (existsSync6(teamPath)) {
|
|
4786
|
-
teamConfig = JSON.parse(await
|
|
6063
|
+
teamConfig = JSON.parse(await readFile3(teamPath, "utf8"));
|
|
4787
6064
|
}
|
|
4788
6065
|
return resolveLayeredConfig({
|
|
4789
6066
|
repoConfig: loaded,
|
|
@@ -4928,7 +6205,7 @@ async function evaluateGatedAction(ctx, deps, params) {
|
|
|
4928
6205
|
command: params.command,
|
|
4929
6206
|
cwd: params.cwd,
|
|
4930
6207
|
repoRoot: ctx.repoRoot,
|
|
4931
|
-
stateDir:
|
|
6208
|
+
stateDir: path20.join(ctx.layout.repoLocalStateDir(ctx.repoRoot), "transactional"),
|
|
4932
6209
|
timeoutMs: transactional.timeoutMs,
|
|
4933
6210
|
predicted,
|
|
4934
6211
|
diffContext: {
|
|
@@ -5025,7 +6302,17 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
5025
6302
|
});
|
|
5026
6303
|
}
|
|
5027
6304
|
const brokerActive = isCapabilityBrokerDemotionActive(ctx.config);
|
|
5028
|
-
|
|
6305
|
+
let approved = null;
|
|
6306
|
+
if (!TRANSACTIONAL_APPROVAL_BYPASS_REASONS.has(result.reason) && !shouldSkipBrokerApprovedOnce(brokerActive, result.reason)) {
|
|
6307
|
+
const approvedState = await deps.loadApprovals(ctx, "approved-approvals.json");
|
|
6308
|
+
approvedState.state = compactApprovals(approvedState.state);
|
|
6309
|
+
const matchedApproval = approvedState.state.approvals.find(
|
|
6310
|
+
(entry) => entry.kind === kind && entry.fingerprint === result.fingerprint && entry.repoRoot === ctx.repoRoot
|
|
6311
|
+
);
|
|
6312
|
+
if (!shouldSkipBrokerApprovedRecord(brokerActive, matchedApproval?.reason)) {
|
|
6313
|
+
approved = await consumeApprovedApproval(ctx, deps, kind, result.fingerprint);
|
|
6314
|
+
}
|
|
6315
|
+
}
|
|
5029
6316
|
if (approved) {
|
|
5030
6317
|
await deps.appendAudit(ctx, {
|
|
5031
6318
|
...gateBase,
|
|
@@ -5203,18 +6490,18 @@ async function appendObservedAudit(ctx, deps, eventName, payload) {
|
|
|
5203
6490
|
|
|
5204
6491
|
// src/adapters/shared/repo-root.ts
|
|
5205
6492
|
import { existsSync as existsSync7 } from "node:fs";
|
|
5206
|
-
import
|
|
6493
|
+
import path21 from "node:path";
|
|
5207
6494
|
function findRepoRoot(startPath, layout) {
|
|
5208
|
-
let current =
|
|
6495
|
+
let current = path21.resolve(startPath);
|
|
5209
6496
|
while (true) {
|
|
5210
6497
|
for (const marker of layout.repoRootMarkers) {
|
|
5211
|
-
if (existsSync7(
|
|
6498
|
+
if (existsSync7(path21.join(current, marker))) {
|
|
5212
6499
|
return current;
|
|
5213
6500
|
}
|
|
5214
6501
|
}
|
|
5215
|
-
const parent =
|
|
6502
|
+
const parent = path21.dirname(current);
|
|
5216
6503
|
if (parent === current) {
|
|
5217
|
-
return
|
|
6504
|
+
return path21.resolve(startPath);
|
|
5218
6505
|
}
|
|
5219
6506
|
current = parent;
|
|
5220
6507
|
}
|