@guilz-dev/belay 0.1.1 → 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 +55 -15
- package/dist/adapters/shared/gate-runtime.js +25 -9
- package/dist/bundle/claude-runtime.mjs +1786 -379
- package/dist/bundle/codex-runtime.mjs +1785 -379
- package/dist/bundle/cursor-runtime.mjs +1786 -379
- package/dist/cli.js +201 -11
- package/dist/commands/approve.js +11 -0
- package/dist/commands/classify-for-report.js +3 -3
- package/dist/commands/config.d.ts +70 -0
- package/dist/commands/config.js +494 -0
- package/dist/commands/doctor.js +3 -2
- package/dist/commands/explain.js +14 -14
- 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/recover.js +2 -2
- 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/approval.d.ts +3 -0
- package/dist/core/approval.js +18 -3
- package/dist/core/audit-io.d.ts +2 -0
- package/dist/core/audit-io.js +14 -0
- package/dist/core/audit-query.js +5 -1
- 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 +23 -3
- package/dist/core/config.js +112 -13
- package/dist/core/credential-store.d.ts +11 -0
- package/dist/core/credential-store.js +60 -0
- package/dist/core/gate-contract.d.ts +1 -1
- package/dist/core/gate-contract.js +1 -1
- package/dist/core/gate-engine.js +105 -14
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +2 -2
- 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 +45 -4
- package/dist/core/judge-config.js +266 -44
- package/dist/core/judge-doctor.d.ts +6 -1
- package/dist/core/judge-doctor.js +148 -97
- 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 +7 -3
- package/dist/core/{v2 → verdict}/adapter.d.ts +1 -0
- package/dist/core/{v2 → verdict}/adapter.js +16 -4
- package/dist/core/{v2 → verdict}/containment.d.ts +5 -0
- package/dist/core/{v2 → verdict}/containment.js +32 -2
- package/dist/core/{v2 → verdict}/egress-classify.js +3 -0
- 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/{v2 → verdict}/judge-factory.d.ts +15 -4
- package/dist/core/verdict/judge-factory.js +178 -0
- package/dist/core/{v2 → verdict}/judge.d.ts +20 -1
- package/dist/core/{v2 → verdict}/judge.js +85 -18
- package/dist/core/{v2 → verdict}/launcher-resolve.js +72 -1
- package/dist/core/verdict/persistent-paths.d.ts +8 -0
- package/dist/core/verdict/persistent-paths.js +52 -0
- package/dist/core/{v2 → verdict}/types.d.ts +6 -2
- package/dist/core/{v2 → verdict}/verdict.js +176 -46
- package/dist/corpus/evaluate.js +2 -2
- package/dist/installer.js +66 -14
- package/dist/types.d.ts +8 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
- package/skills/belay/SKILL.md +8 -7
- package/dist/commands/init-wizard.d.ts +0 -16
- package/dist/commands/init-wizard.js +0 -50
- package/dist/core/v2/judge-factory.js +0 -75
- /package/dist/core/{v2 → verdict}/egress-classify.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/fingerprint.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/fingerprint.js +0 -0
- /package/dist/core/{v2 → verdict}/index.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/index.js +0 -0
- /package/dist/core/{v2 → verdict}/judge-audit.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/judge-audit.js +0 -0
- /package/dist/core/{v2 → verdict}/judge-outbound.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/judge-outbound.js +0 -0
- /package/dist/core/{v2 → verdict}/launcher-resolve.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/overrides.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/overrides.js +0 -0
- /package/dist/core/{v2 → verdict}/parser.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/parser.js +0 -0
- /package/dist/core/{v2 → verdict}/types.js +0 -0
- /package/dist/core/{v2 → verdict}/verdict.d.ts +0 -0
|
@@ -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
|
|
@@ -86,7 +202,7 @@ var LEGACY_POLICY_V3 = {
|
|
|
86
202
|
fenceWarnThreshold: DEFAULT_FENCE_WARN_THRESHOLD
|
|
87
203
|
};
|
|
88
204
|
var DEFAULT_POLICY_V3 = {
|
|
89
|
-
unknownLocalEffect: "
|
|
205
|
+
unknownLocalEffect: "allow_flagged",
|
|
90
206
|
unparseableShell: "deny",
|
|
91
207
|
codexUnmappedTool: "deny",
|
|
92
208
|
confidenceThresholds: { ...DEFAULT_CONFIDENCE_THRESHOLDS },
|
|
@@ -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,20 +905,29 @@ 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
|
|
912
|
+
var APPROVAL_EXECUTION_LEASE_MS = 6e4;
|
|
707
913
|
function nowIso() {
|
|
708
914
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
709
915
|
}
|
|
710
916
|
function isExpired(approval) {
|
|
711
917
|
return Date.parse(approval.expiresAt) <= Date.now();
|
|
712
918
|
}
|
|
919
|
+
function isExecutionLeaseExpired(approval) {
|
|
920
|
+
if (!approval.executionLeaseExpiresAt) {
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
return Date.parse(approval.executionLeaseExpiresAt) <= Date.now();
|
|
924
|
+
}
|
|
713
925
|
function compactApprovals(state) {
|
|
714
926
|
return {
|
|
715
927
|
version: state.version,
|
|
716
|
-
approvals: state.approvals.filter(
|
|
928
|
+
approvals: state.approvals.filter(
|
|
929
|
+
(approval) => !isExpired(approval) && !isExecutionLeaseExpired(approval)
|
|
930
|
+
)
|
|
717
931
|
};
|
|
718
932
|
}
|
|
719
933
|
function escapeRegex(value) {
|
|
@@ -722,8 +936,15 @@ function escapeRegex(value) {
|
|
|
722
936
|
}
|
|
723
937
|
function approvalCommandMatch(prompt, tokenPrefix) {
|
|
724
938
|
const escapedPrefix = escapeRegex(tokenPrefix);
|
|
725
|
-
const
|
|
726
|
-
|
|
939
|
+
const linePattern = new RegExp(`^\\s*${escapedPrefix}\\s+(\\S+)\\s*$`, "i");
|
|
940
|
+
for (const line of prompt.split(/\r?\n/)) {
|
|
941
|
+
if (!line.trim()) {
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
const match = line.match(linePattern);
|
|
945
|
+
return match?.[1] ?? null;
|
|
946
|
+
}
|
|
947
|
+
return null;
|
|
727
948
|
}
|
|
728
949
|
function buildRetryInstruction(tokenPrefix, approvalId) {
|
|
729
950
|
return `To allow the next matching action once, send ${tokenPrefix} ${approvalId} and then retry the original action unchanged.`;
|
|
@@ -956,6 +1177,9 @@ function isCapabilityBrokerDemotionActive(config) {
|
|
|
956
1177
|
return hasSandboxRuntime(config);
|
|
957
1178
|
}
|
|
958
1179
|
|
|
1180
|
+
// src/core/capability/paths.ts
|
|
1181
|
+
import path7 from "node:path";
|
|
1182
|
+
|
|
959
1183
|
// src/core/shell-tokenizer.ts
|
|
960
1184
|
function tokenizeShell(input) {
|
|
961
1185
|
const tokens = [];
|
|
@@ -1053,33 +1277,106 @@ function extractRedirectTargets(tokens) {
|
|
|
1053
1277
|
}
|
|
1054
1278
|
|
|
1055
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
|
+
}
|
|
1056
1327
|
function collectOutsideRepoPaths(command, cwd, repoRoot) {
|
|
1057
1328
|
const tokens = tokenizeShell(command);
|
|
1058
1329
|
const redirects = extractRedirectTargets(tokens);
|
|
1059
1330
|
const paths = /* @__PURE__ */ new Set();
|
|
1060
1331
|
for (const token of tokens.slice(1)) {
|
|
1061
|
-
|
|
1062
|
-
if (resolved && relativeWithinRepo(repoRoot, resolved) === null) {
|
|
1063
|
-
paths.add(resolved);
|
|
1064
|
-
}
|
|
1332
|
+
addOutsideRepoPath(paths, token, cwd, repoRoot);
|
|
1065
1333
|
}
|
|
1066
1334
|
for (const redirect of redirects) {
|
|
1067
|
-
|
|
1068
|
-
|
|
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) {
|
|
1069
1346
|
paths.add(resolved);
|
|
1070
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
|
+
}
|
|
1071
1360
|
}
|
|
1072
1361
|
return [...paths];
|
|
1073
1362
|
}
|
|
1074
1363
|
|
|
1075
1364
|
// src/core/capability/reasons.ts
|
|
1076
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
|
+
]);
|
|
1077
1371
|
function shouldSkipBrokerApprovedOnce(brokerActive, reason) {
|
|
1078
1372
|
return brokerActive && FS_SCOPE_REASONS.has(reason);
|
|
1079
1373
|
}
|
|
1374
|
+
function shouldSkipBrokerApprovedRecord(brokerActive, approvalReason) {
|
|
1375
|
+
return brokerActive && approvalReason !== void 0 && FS_SCOPE_REASONS.has(approvalReason);
|
|
1376
|
+
}
|
|
1080
1377
|
|
|
1081
1378
|
// src/core/config-layers.ts
|
|
1082
|
-
import
|
|
1379
|
+
import path8 from "node:path";
|
|
1083
1380
|
|
|
1084
1381
|
// src/presets.ts
|
|
1085
1382
|
var CONFIG_PRESETS = {
|
|
@@ -1179,8 +1476,8 @@ function applyConfigPreset(preset, extra = {}) {
|
|
|
1179
1476
|
// src/core/config-layers.ts
|
|
1180
1477
|
function teamConfigPath(homedir = () => process.env.HOME ?? process.env.USERPROFILE ?? "") {
|
|
1181
1478
|
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
1182
|
-
const base = xdg ||
|
|
1183
|
-
return
|
|
1479
|
+
const base = xdg || path8.join(homedir(), ".config");
|
|
1480
|
+
return path8.join(base, "agent-belay", "team.config.json");
|
|
1184
1481
|
}
|
|
1185
1482
|
function applyProtectedLayer(config, builtin) {
|
|
1186
1483
|
const controlPlane = { ...config.controlPlane };
|
|
@@ -1214,6 +1511,10 @@ function resolveLayeredConfig(params) {
|
|
|
1214
1511
|
if (params.teamConfig) {
|
|
1215
1512
|
const teamFile = params.teamConfig;
|
|
1216
1513
|
const teamRaw = teamFile.preset ? applyConfigPreset(teamFile.preset, teamFile.config ?? {}) : teamFile.config ?? params.teamConfig;
|
|
1514
|
+
rejectTeamLayerJudgeSecrets(
|
|
1515
|
+
teamRaw.judge,
|
|
1516
|
+
"team"
|
|
1517
|
+
);
|
|
1217
1518
|
config = mergeConfigLayer(config, asV3Layer(teamRaw));
|
|
1218
1519
|
provenance.push({
|
|
1219
1520
|
path: params.teamConfigPath ?? teamConfigPath(),
|
|
@@ -1250,7 +1551,7 @@ function classifyResultToGateVerdict(params) {
|
|
|
1250
1551
|
approvalId,
|
|
1251
1552
|
user_message,
|
|
1252
1553
|
agent_message,
|
|
1253
|
-
|
|
1554
|
+
axes: result.axes
|
|
1254
1555
|
};
|
|
1255
1556
|
}
|
|
1256
1557
|
function unnormalizedGateVerdict(params) {
|
|
@@ -1459,7 +1760,7 @@ function classifySubagent(payload, repoRoot, options = {}) {
|
|
|
1459
1760
|
}
|
|
1460
1761
|
|
|
1461
1762
|
// src/core/classify-tool.ts
|
|
1462
|
-
import
|
|
1763
|
+
import path16 from "node:path";
|
|
1463
1764
|
|
|
1464
1765
|
// src/core/glob.ts
|
|
1465
1766
|
function matchesSensitivePath(filePath, patterns) {
|
|
@@ -1504,7 +1805,7 @@ function matchesSensitivePath(filePath, patterns) {
|
|
|
1504
1805
|
return false;
|
|
1505
1806
|
}
|
|
1506
1807
|
|
|
1507
|
-
// src/core/
|
|
1808
|
+
// src/core/verdict/judge-audit.ts
|
|
1508
1809
|
function judgeTraceAuditFields(trace) {
|
|
1509
1810
|
if (!trace) {
|
|
1510
1811
|
return {};
|
|
@@ -1519,55 +1820,464 @@ function judgeTraceAuditFields(trace) {
|
|
|
1519
1820
|
};
|
|
1520
1821
|
}
|
|
1521
1822
|
|
|
1522
|
-
// src/
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
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
|
+
};
|
|
1533
1860
|
}
|
|
1534
|
-
|
|
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);
|
|
1535
1868
|
}
|
|
1536
|
-
|
|
1537
|
-
|
|
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];
|
|
1538
1911
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
return true;
|
|
1545
|
-
}
|
|
1912
|
+
|
|
1913
|
+
// src/config-io.ts
|
|
1914
|
+
function resolveAdapterName(config) {
|
|
1915
|
+
if (config.adapter === "claude") {
|
|
1916
|
+
return "claude";
|
|
1546
1917
|
}
|
|
1547
|
-
|
|
1918
|
+
if (config.adapter === "codex") {
|
|
1919
|
+
return "codex";
|
|
1920
|
+
}
|
|
1921
|
+
return "cursor";
|
|
1548
1922
|
}
|
|
1549
|
-
function
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
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);
|
|
1553
1978
|
return true;
|
|
1979
|
+
} catch {
|
|
1554
1980
|
}
|
|
1555
1981
|
}
|
|
1556
1982
|
return false;
|
|
1557
1983
|
}
|
|
1558
|
-
function
|
|
1559
|
-
const
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
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 };
|
|
1563
1988
|
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
if (segments.length > 1) {
|
|
1567
|
-
return token.replace(basename, "[REDACTED]");
|
|
1989
|
+
if (env.BELAY_JUDGE_DISABLE_CLI_TRANSPORT === "1") {
|
|
1990
|
+
return { http: true, cliTransport: null };
|
|
1568
1991
|
}
|
|
1569
|
-
|
|
1570
|
-
}
|
|
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 };
|
|
2000
|
+
}
|
|
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
|
+
}
|
|
1571
2281
|
function scrubOutboundForJudge(text, options) {
|
|
1572
2282
|
try {
|
|
1573
2283
|
let scrubbed = scrubString(text, {
|
|
@@ -1588,37 +2298,57 @@ function scrubOutboundForJudge(text, options) {
|
|
|
1588
2298
|
}
|
|
1589
2299
|
}
|
|
1590
2300
|
|
|
1591
|
-
// src/core/
|
|
2301
|
+
// src/core/verdict/judge.ts
|
|
1592
2302
|
var SECRET_PATTERNS = [/\.env\b/i, /\.pem\b/i, /id_rsa\b/i, /credentials/i, /secrets?\b/i];
|
|
1593
2303
|
var DESTRUCTIVE_VERBS = /\b(rm|rmtree|unlink|delete|truncate|shred|destroy|drop)\b/i;
|
|
1594
2304
|
var GIT_PATTERNS = /\.git\b/i;
|
|
1595
|
-
var TIER1_PROMPT = `Answer ONLY with JSON: {"
|
|
1596
|
-
|
|
2305
|
+
var TIER1_PROMPT = `Answer ONLY with JSON: {"local_recoverable":boolean,"destroys_history_or_secrets":boolean,"reason":string}
|
|
2306
|
+
reason should be a short snake_case label.
|
|
2307
|
+
|
|
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).
|
|
1597
2312
|
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
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.
|
|
1601
2316
|
|
|
1602
2317
|
Command/code:
|
|
1603
2318
|
`;
|
|
1604
2319
|
function failClosedVerdict(reason) {
|
|
1605
2320
|
return {
|
|
1606
|
-
|
|
2321
|
+
local_recoverable: false,
|
|
1607
2322
|
destroys_outside_repo: true,
|
|
1608
2323
|
destroys_history_or_secrets: true,
|
|
2324
|
+
external_change: true,
|
|
1609
2325
|
reason
|
|
1610
2326
|
};
|
|
1611
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
|
+
}
|
|
1612
2340
|
function parseTier1Json(raw) {
|
|
1613
2341
|
try {
|
|
1614
2342
|
const parsed = JSON.parse(raw);
|
|
1615
|
-
|
|
2343
|
+
const localRecoverable = parseLocalRecoverable(parsed);
|
|
2344
|
+
if (localRecoverable === null) {
|
|
1616
2345
|
return null;
|
|
1617
2346
|
}
|
|
1618
2347
|
return {
|
|
1619
|
-
|
|
1620
|
-
destroys_outside_repo: parsed.destroys_outside_repo
|
|
1621
|
-
destroys_history_or_secrets: parsed.destroys_history_or_secrets
|
|
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,
|
|
2351
|
+
external_change: parsed.external_change,
|
|
1622
2352
|
reason: typeof parsed.reason === "string" ? parsed.reason : "tier1_llm"
|
|
1623
2353
|
};
|
|
1624
2354
|
} catch {
|
|
@@ -1632,7 +2362,7 @@ function prescanInterpreterCode(code) {
|
|
|
1632
2362
|
const hitsDestructive = DESTRUCTIVE_VERBS.test(normalized);
|
|
1633
2363
|
if ((hitsSecret || hitsGit) && hitsDestructive) {
|
|
1634
2364
|
return {
|
|
1635
|
-
|
|
2365
|
+
local_recoverable: true,
|
|
1636
2366
|
destroys_outside_repo: false,
|
|
1637
2367
|
destroys_history_or_secrets: true,
|
|
1638
2368
|
reason: "prescan_destructive_secret"
|
|
@@ -1640,11 +2370,49 @@ function prescanInterpreterCode(code) {
|
|
|
1640
2370
|
}
|
|
1641
2371
|
return null;
|
|
1642
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
|
+
}
|
|
1643
2411
|
function createDeterministicJudgeStub() {
|
|
1644
2412
|
return {
|
|
1645
2413
|
evaluate() {
|
|
1646
2414
|
return Promise.resolve({
|
|
1647
|
-
|
|
2415
|
+
local_recoverable: true,
|
|
1648
2416
|
destroys_outside_repo: false,
|
|
1649
2417
|
destroys_history_or_secrets: false,
|
|
1650
2418
|
reason: "deterministic_stub"
|
|
@@ -1776,8 +2544,7 @@ function createOpenAiCompatibleJudge(options) {
|
|
|
1776
2544
|
};
|
|
1777
2545
|
return failClosedVerdict("outbound_scrub_failed");
|
|
1778
2546
|
}
|
|
1779
|
-
const
|
|
1780
|
-
const resolvedKey = resolveJudgeApiKey2();
|
|
2547
|
+
const resolvedKey = options.resolveApiKey ? await options.resolveApiKey() : { key: options.apiKey ?? null, source: null };
|
|
1781
2548
|
const apiKey = options.apiKey ?? resolvedKey.key;
|
|
1782
2549
|
if (!apiKey) {
|
|
1783
2550
|
judge.lastTrace = {
|
|
@@ -1841,43 +2608,385 @@ function createOpenAiCompatibleJudge(options) {
|
|
|
1841
2608
|
return judge;
|
|
1842
2609
|
}
|
|
1843
2610
|
function tier1RequiresAsk(verdict2) {
|
|
1844
|
-
return verdict2.
|
|
2611
|
+
return !verdict2.local_recoverable || verdict2.destroys_history_or_secrets;
|
|
1845
2612
|
}
|
|
1846
2613
|
|
|
1847
|
-
// src/core/
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
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") {
|
|
1852
2650
|
return {
|
|
1853
|
-
|
|
1854
|
-
|
|
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
|
|
1855
2663
|
};
|
|
1856
2664
|
}
|
|
1857
|
-
|
|
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
|
+
};
|
|
2699
|
+
}
|
|
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
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
return null;
|
|
2719
|
+
}
|
|
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
|
+
}
|
|
2732
|
+
}
|
|
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;
|
|
2740
|
+
}
|
|
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;
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
return null;
|
|
2754
|
+
}
|
|
2755
|
+
function parseCliJudgeOutput(providerId, raw) {
|
|
2756
|
+
const direct = parseTier1Json(raw);
|
|
2757
|
+
if (direct) {
|
|
2758
|
+
return direct;
|
|
2759
|
+
}
|
|
2760
|
+
const parsed = tryParseJson(raw);
|
|
2761
|
+
if (parsed) {
|
|
2762
|
+
const envelopeVerdict = extractVerdictFromJsonValue(parsed);
|
|
2763
|
+
if (envelopeVerdict) {
|
|
2764
|
+
return envelopeVerdict;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
if (providerId === "codex") {
|
|
2768
|
+
return parseCliJsonLines(raw);
|
|
2769
|
+
}
|
|
2770
|
+
return parseCliJsonLines(raw);
|
|
2771
|
+
}
|
|
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;
|
|
2903
|
+
}
|
|
2904
|
+
return process.env.BELAY_JUDGE_MODEL_RESOLVED?.trim() || void 0;
|
|
2905
|
+
}
|
|
2906
|
+
function providerIdFromJudge(judge) {
|
|
2907
|
+
if (judge.providerId) {
|
|
2908
|
+
const normalized = normalizeLegacyProviderId(judge.providerId);
|
|
2909
|
+
if (normalized) {
|
|
2910
|
+
return normalized;
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
return inferProviderIdFromConfig(judge);
|
|
2914
|
+
}
|
|
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);
|
|
2926
|
+
}
|
|
2927
|
+
if (providerId === "cursor") {
|
|
2928
|
+
return createCursorCliJudge(options);
|
|
2929
|
+
}
|
|
2930
|
+
return createClaudeCliJudge(options);
|
|
1858
2931
|
}
|
|
1859
2932
|
function createJudgeFromConfig(config, options = {}) {
|
|
2933
|
+
if (process.env.BELAY_DETERMINISTIC_JUDGE === "1") {
|
|
2934
|
+
return createDeterministicJudgeStub();
|
|
2935
|
+
}
|
|
1860
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
|
+
}
|
|
1861
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);
|
|
1862
2951
|
if (provider === "openai-compatible") {
|
|
1863
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
|
+
}
|
|
1864
2958
|
if (!endpoint) {
|
|
1865
2959
|
return createFailClosedJudge({
|
|
1866
2960
|
reason: "openai_compatible_endpoint_missing",
|
|
1867
2961
|
fallbackReason: "missing_endpoint",
|
|
1868
2962
|
modelRequested: judgeConfig.model,
|
|
1869
|
-
modelResolved:
|
|
2963
|
+
modelResolved: resolved
|
|
1870
2964
|
});
|
|
1871
2965
|
}
|
|
1872
|
-
|
|
1873
|
-
|
|
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
|
+
});
|
|
2973
|
+
}
|
|
2974
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
2975
|
+
const repoLocalStateDir = repoLocalStateDirFor(repoRoot, config);
|
|
1874
2976
|
return createOpenAiCompatibleJudge({
|
|
1875
2977
|
endpoint,
|
|
1876
2978
|
modelRequested: judgeConfig.model,
|
|
1877
2979
|
modelResolved: resolved,
|
|
1878
2980
|
timeoutMs: judgeConfig.timeoutMs,
|
|
1879
2981
|
sensitivePaths: config.classifier.sensitivePaths,
|
|
1880
|
-
scrubOptions: scrubOptionsFromConfig(config)
|
|
2982
|
+
scrubOptions: scrubOptionsFromConfig(config),
|
|
2983
|
+
resolveApiKey: async () => resolveJudgeCredential({
|
|
2984
|
+
judge: judgeConfig,
|
|
2985
|
+
catalogSpec: catalogSpec ?? void 0,
|
|
2986
|
+
repoRoot,
|
|
2987
|
+
repoLocalStateDir,
|
|
2988
|
+
config
|
|
2989
|
+
})
|
|
1881
2990
|
});
|
|
1882
2991
|
}
|
|
1883
2992
|
if (provider === "ollama") {
|
|
@@ -1888,105 +2997,32 @@ function createJudgeFromConfig(config, options = {}) {
|
|
|
1888
2997
|
keepAlive: judgeConfig.keepAlive
|
|
1889
2998
|
});
|
|
1890
2999
|
}
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
if (token === "~" || token.startsWith("~/")) {
|
|
1901
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
1902
|
-
if (!home) {
|
|
1903
|
-
return token;
|
|
1904
|
-
}
|
|
1905
|
-
return token === "~" ? home : path8.join(home, token.slice(2));
|
|
1906
|
-
}
|
|
1907
|
-
return token;
|
|
1908
|
-
}
|
|
1909
|
-
function resolveTrustedPath(token, trustedCwd, trusted) {
|
|
1910
|
-
if (!token || token === "--" || token.startsWith("-")) {
|
|
1911
|
-
return null;
|
|
1912
|
-
}
|
|
1913
|
-
if (!trusted || !trustedCwd) {
|
|
1914
|
-
return null;
|
|
1915
|
-
}
|
|
1916
|
-
const expanded = expandHome(token);
|
|
1917
|
-
if (path8.isAbsolute(expanded)) {
|
|
1918
|
-
return canonicalPath(expanded);
|
|
1919
|
-
}
|
|
1920
|
-
return canonicalPath(path8.resolve(trustedCwd, expanded));
|
|
1921
|
-
}
|
|
1922
|
-
function locationForPath(resolvedPath, repoRoot) {
|
|
1923
|
-
if (!resolvedPath) {
|
|
1924
|
-
return "unknown";
|
|
1925
|
-
}
|
|
1926
|
-
if (pathWithinRoot(repoRoot, resolvedPath)) {
|
|
1927
|
-
return "repo_local";
|
|
1928
|
-
}
|
|
1929
|
-
return "repo_outside";
|
|
1930
|
-
}
|
|
1931
|
-
function isGitPath(resolvedPath, repoRoot) {
|
|
1932
|
-
const relative = relativeWithinRepo(repoRoot, resolvedPath);
|
|
1933
|
-
if (!relative) {
|
|
1934
|
-
return false;
|
|
1935
|
-
}
|
|
1936
|
-
const normalized = relative.replaceAll("\\", "/");
|
|
1937
|
-
return normalized === ".git" || normalized.startsWith(".git/");
|
|
1938
|
-
}
|
|
1939
|
-
function isHighStakesPath(resolvedPath, repoRoot, sensitivePaths, protectedRoots = []) {
|
|
1940
|
-
if (isGitPath(resolvedPath, repoRoot)) {
|
|
1941
|
-
return true;
|
|
1942
|
-
}
|
|
1943
|
-
const relative = relativeWithinRepo(repoRoot, resolvedPath);
|
|
1944
|
-
const checkPath = relative ?? resolvedPath;
|
|
1945
|
-
if (matchesSensitivePath(checkPath.replaceAll("\\", "/"), sensitivePaths)) {
|
|
1946
|
-
return true;
|
|
1947
|
-
}
|
|
1948
|
-
return protectedRoots.some((root) => pathWithinRoot(root, resolvedPath));
|
|
1949
|
-
}
|
|
1950
|
-
function analyzePathTargets(params) {
|
|
1951
|
-
const signals = [];
|
|
1952
|
-
if (!params.trustedCwd || !params.cwd) {
|
|
1953
|
-
return {
|
|
1954
|
-
location: "unknown",
|
|
1955
|
-
isHighStakes: false,
|
|
1956
|
-
signals: ["missing_trusted_cwd"]
|
|
1957
|
-
};
|
|
1958
|
-
}
|
|
1959
|
-
const locations = /* @__PURE__ */ new Set();
|
|
1960
|
-
let isHighStakes = false;
|
|
1961
|
-
for (const target of params.targets) {
|
|
1962
|
-
const resolved = resolveTrustedPath(target, params.cwd, params.trustedCwd) ?? resolveMutationTarget(target, params.cwd);
|
|
1963
|
-
const location2 = locationForPath(resolved, params.repoRoot);
|
|
1964
|
-
locations.add(location2);
|
|
1965
|
-
if (resolved && isHighStakesPath(
|
|
1966
|
-
resolved,
|
|
1967
|
-
params.repoRoot,
|
|
1968
|
-
params.sensitivePaths,
|
|
1969
|
-
params.protectedArtifactRoots
|
|
1970
|
-
)) {
|
|
1971
|
-
isHighStakes = true;
|
|
1972
|
-
signals.push("high_stakes_path");
|
|
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
|
+
});
|
|
1973
3009
|
}
|
|
3010
|
+
return createFailClosedJudge({
|
|
3011
|
+
reason: "anthropic_not_implemented",
|
|
3012
|
+
fallbackReason: "anthropic_runtime_unavailable",
|
|
3013
|
+
modelRequested: judgeConfig.model,
|
|
3014
|
+
modelResolved: resolved
|
|
3015
|
+
});
|
|
1974
3016
|
}
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
}
|
|
1981
|
-
location = "mixed";
|
|
1982
|
-
}
|
|
1983
|
-
return { location, isHighStakes, signals };
|
|
1984
|
-
}
|
|
1985
|
-
function cwdRelative(repoRoot, cwd) {
|
|
1986
|
-
return relativeWithinRepo(repoRoot, cwd) ?? cwd;
|
|
3017
|
+
return createFailClosedJudge({
|
|
3018
|
+
reason: "unsupported_judge_provider",
|
|
3019
|
+
fallbackReason: "unsupported_provider",
|
|
3020
|
+
modelRequested: judgeConfig.model,
|
|
3021
|
+
modelResolved: resolved
|
|
3022
|
+
});
|
|
1987
3023
|
}
|
|
1988
3024
|
|
|
1989
|
-
// src/core/
|
|
3025
|
+
// src/core/verdict/egress-classify.ts
|
|
1990
3026
|
var EGRESS_TOOL_HEADS = /* @__PURE__ */ new Set([
|
|
1991
3027
|
"aws",
|
|
1992
3028
|
"curl",
|
|
@@ -2084,6 +3120,9 @@ function classifyAws(tokens) {
|
|
|
2084
3120
|
if (/\bs3\s+rm\b/.test(joined)) {
|
|
2085
3121
|
return "destructive";
|
|
2086
3122
|
}
|
|
3123
|
+
if (/\bs3\s+mb\b/.test(joined)) {
|
|
3124
|
+
return "destructive";
|
|
3125
|
+
}
|
|
2087
3126
|
if (/\bs3\s+sync\b/.test(joined)) {
|
|
2088
3127
|
return "destructive";
|
|
2089
3128
|
}
|
|
@@ -2194,17 +3233,61 @@ function classifyNetlify(tokens) {
|
|
|
2194
3233
|
return "ambiguous";
|
|
2195
3234
|
}
|
|
2196
3235
|
|
|
2197
|
-
// src/core/
|
|
3236
|
+
// src/core/verdict/fingerprint.ts
|
|
2198
3237
|
function verdictFingerprint(cwdRelative2, commandRedacted) {
|
|
2199
3238
|
return hashValue(`v2:${cwdRelative2}:${commandRedacted}`);
|
|
2200
3239
|
}
|
|
2201
3240
|
|
|
2202
|
-
// src/core/
|
|
3241
|
+
// src/core/verdict/launcher-resolve.ts
|
|
2203
3242
|
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
|
|
2204
|
-
import
|
|
3243
|
+
import path14 from "node:path";
|
|
2205
3244
|
var MAX_RESOLVE_DEPTH = 8;
|
|
3245
|
+
var PNPM_BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
|
|
3246
|
+
"add",
|
|
3247
|
+
"audit",
|
|
3248
|
+
"cache",
|
|
3249
|
+
"config",
|
|
3250
|
+
"deploy",
|
|
3251
|
+
"dlx",
|
|
3252
|
+
"exec",
|
|
3253
|
+
"fetch",
|
|
3254
|
+
"help",
|
|
3255
|
+
"import",
|
|
3256
|
+
"init",
|
|
3257
|
+
"install",
|
|
3258
|
+
"i",
|
|
3259
|
+
"licenses",
|
|
3260
|
+
"link",
|
|
3261
|
+
"list",
|
|
3262
|
+
"outdated",
|
|
3263
|
+
"pack",
|
|
3264
|
+
"patch",
|
|
3265
|
+
"patch-commit",
|
|
3266
|
+
"patch-remove",
|
|
3267
|
+
"publish",
|
|
3268
|
+
"prune",
|
|
3269
|
+
"rebuild",
|
|
3270
|
+
"remove",
|
|
3271
|
+
"rm",
|
|
3272
|
+
"store",
|
|
3273
|
+
"unlink",
|
|
3274
|
+
"update",
|
|
3275
|
+
"up",
|
|
3276
|
+
"why"
|
|
3277
|
+
]);
|
|
3278
|
+
var PNPM_EXEC_LIKE_HEADS = /* @__PURE__ */ new Set([
|
|
3279
|
+
"vitest",
|
|
3280
|
+
"vite",
|
|
3281
|
+
"biome",
|
|
3282
|
+
"eslint",
|
|
3283
|
+
"jest",
|
|
3284
|
+
"mocha",
|
|
3285
|
+
"tsc",
|
|
3286
|
+
"tsx",
|
|
3287
|
+
"node"
|
|
3288
|
+
]);
|
|
2206
3289
|
function readPackageJson(dir) {
|
|
2207
|
-
const packagePath =
|
|
3290
|
+
const packagePath = path14.join(dir, "package.json");
|
|
2208
3291
|
if (!existsSync4(packagePath)) {
|
|
2209
3292
|
return null;
|
|
2210
3293
|
}
|
|
@@ -2215,17 +3298,17 @@ function readPackageJson(dir) {
|
|
|
2215
3298
|
}
|
|
2216
3299
|
}
|
|
2217
3300
|
function findPackageJson(startDir, stopDir) {
|
|
2218
|
-
let current =
|
|
2219
|
-
const stop =
|
|
3301
|
+
let current = path14.resolve(startDir);
|
|
3302
|
+
const stop = path14.resolve(stopDir);
|
|
2220
3303
|
while (true) {
|
|
2221
|
-
const packagePath =
|
|
3304
|
+
const packagePath = path14.join(current, "package.json");
|
|
2222
3305
|
if (existsSync4(packagePath)) {
|
|
2223
3306
|
return packagePath;
|
|
2224
3307
|
}
|
|
2225
|
-
if (current === stop || current ===
|
|
3308
|
+
if (current === stop || current === path14.dirname(current)) {
|
|
2226
3309
|
return existsSync4(packagePath) ? packagePath : null;
|
|
2227
3310
|
}
|
|
2228
|
-
const parent =
|
|
3311
|
+
const parent = path14.dirname(current);
|
|
2229
3312
|
if (!parent.startsWith(stop) && parent !== current) {
|
|
2230
3313
|
}
|
|
2231
3314
|
if (parent === current) {
|
|
@@ -2257,6 +3340,12 @@ function npmScriptName(tokens) {
|
|
|
2257
3340
|
if (launcher[0] === "pnpm" && launcher[1] === "run" && launcher[2]) {
|
|
2258
3341
|
return launcher[2];
|
|
2259
3342
|
}
|
|
3343
|
+
if (launcher[0] === "pnpm" && launcher[1] === "test") {
|
|
3344
|
+
return "test";
|
|
3345
|
+
}
|
|
3346
|
+
if (launcher[0] === "pnpm" && launcher[1] && !launcher[1].startsWith("-") && !PNPM_BUILTIN_COMMANDS.has(launcher[1])) {
|
|
3347
|
+
return launcher[1];
|
|
3348
|
+
}
|
|
2260
3349
|
if (launcher[0] === "npm" && launcher[1] && launcher[1] !== "run" && launcher[1] !== "install") {
|
|
2261
3350
|
return null;
|
|
2262
3351
|
}
|
|
@@ -2276,7 +3365,7 @@ function resolveNpmRecipe(cwd, repoRoot, scriptName, extraArgs) {
|
|
|
2276
3365
|
}
|
|
2277
3366
|
return { recipes: [], opaque: true, reason: "package_json_missing" };
|
|
2278
3367
|
}
|
|
2279
|
-
const pkg = readPackageJson(
|
|
3368
|
+
const pkg = readPackageJson(path14.dirname(packagePath));
|
|
2280
3369
|
const scripts = pkg?.scripts;
|
|
2281
3370
|
if (!scripts || typeof scripts !== "object") {
|
|
2282
3371
|
return { recipes: [], opaque: true, reason: "package_scripts_missing" };
|
|
@@ -2341,20 +3430,20 @@ function parseMakefileRecipes(makefilePath) {
|
|
|
2341
3430
|
function resolveMakeRecipe(cwd, repoRoot, target) {
|
|
2342
3431
|
const candidates = ["Makefile", "makefile", "GNUmakefile"];
|
|
2343
3432
|
let makefilePath = null;
|
|
2344
|
-
let searchDir =
|
|
2345
|
-
const stop =
|
|
3433
|
+
let searchDir = path14.resolve(cwd);
|
|
3434
|
+
const stop = path14.resolve(repoRoot);
|
|
2346
3435
|
while (true) {
|
|
2347
3436
|
for (const name of candidates) {
|
|
2348
|
-
const candidate =
|
|
3437
|
+
const candidate = path14.join(searchDir, name);
|
|
2349
3438
|
if (existsSync4(candidate)) {
|
|
2350
3439
|
makefilePath = candidate;
|
|
2351
3440
|
break;
|
|
2352
3441
|
}
|
|
2353
3442
|
}
|
|
2354
|
-
if (makefilePath || searchDir === stop || searchDir ===
|
|
3443
|
+
if (makefilePath || searchDir === stop || searchDir === path14.dirname(searchDir)) {
|
|
2355
3444
|
break;
|
|
2356
3445
|
}
|
|
2357
|
-
searchDir =
|
|
3446
|
+
searchDir = path14.dirname(searchDir);
|
|
2358
3447
|
}
|
|
2359
3448
|
if (!makefilePath) {
|
|
2360
3449
|
return { recipes: [], opaque: true, reason: "unknown_local_effect" };
|
|
@@ -2378,11 +3467,31 @@ function resolveLauncherRecipe(params) {
|
|
|
2378
3467
|
const tokens = params.tokens;
|
|
2379
3468
|
const scriptName = npmScriptName(tokens);
|
|
2380
3469
|
if (scriptName) {
|
|
2381
|
-
|
|
3470
|
+
const resolution = resolveNpmRecipe(
|
|
3471
|
+
params.cwd,
|
|
3472
|
+
params.repoRoot,
|
|
3473
|
+
scriptName,
|
|
3474
|
+
forwardedArgs(tokens)
|
|
3475
|
+
);
|
|
3476
|
+
if (tokens[0] === "pnpm" && tokens[1] && PNPM_EXEC_LIKE_HEADS.has(tokens[1]) && resolution.reason === "npm_script_undefined") {
|
|
3477
|
+
return {
|
|
3478
|
+
recipes: [tokens.slice(1).join(" ")],
|
|
3479
|
+
opaque: false,
|
|
3480
|
+
reason: "pnpm_exec_like"
|
|
3481
|
+
};
|
|
3482
|
+
}
|
|
3483
|
+
return resolution;
|
|
2382
3484
|
}
|
|
2383
3485
|
if (tokens[0] === "make" && tokens[1] && !tokens[1].startsWith("-")) {
|
|
2384
3486
|
return resolveMakeRecipe(params.cwd, params.repoRoot, tokens[1]);
|
|
2385
3487
|
}
|
|
3488
|
+
if (tokens[0] === "pnpm" && tokens[1] && PNPM_EXEC_LIKE_HEADS.has(tokens[1])) {
|
|
3489
|
+
return {
|
|
3490
|
+
recipes: [tokens.slice(1).join(" ")],
|
|
3491
|
+
opaque: false,
|
|
3492
|
+
reason: "pnpm_exec_like"
|
|
3493
|
+
};
|
|
3494
|
+
}
|
|
2386
3495
|
return null;
|
|
2387
3496
|
}
|
|
2388
3497
|
function isRoutineLauncher(tokens) {
|
|
@@ -2398,7 +3507,7 @@ function matchesCustomCommand(normalizedCommand, key, pattern) {
|
|
|
2398
3507
|
return normalizedCommand === trimmed || key === trimmed;
|
|
2399
3508
|
}
|
|
2400
3509
|
|
|
2401
|
-
// src/core/
|
|
3510
|
+
// src/core/verdict/overrides.ts
|
|
2402
3511
|
function matchesCustomPatterns(command, segment, patterns) {
|
|
2403
3512
|
if (!patterns || patterns.length === 0) {
|
|
2404
3513
|
return false;
|
|
@@ -2437,8 +3546,8 @@ function askFromCustomExternal(opacity) {
|
|
|
2437
3546
|
};
|
|
2438
3547
|
}
|
|
2439
3548
|
|
|
2440
|
-
// src/core/
|
|
2441
|
-
import
|
|
3549
|
+
// src/core/verdict/parser.ts
|
|
3550
|
+
import path15 from "node:path";
|
|
2442
3551
|
|
|
2443
3552
|
// src/core/shell-substitution.ts
|
|
2444
3553
|
function findCommandSubstitutions(command) {
|
|
@@ -2661,7 +3770,7 @@ function hasUnbalancedDollarParen(command) {
|
|
|
2661
3770
|
return depth > 0;
|
|
2662
3771
|
}
|
|
2663
3772
|
|
|
2664
|
-
// src/core/
|
|
3773
|
+
// src/core/verdict/parser.ts
|
|
2665
3774
|
var ENV_PREFIX_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=(?:'[^']*'|"[^"]*"|\S+)$/;
|
|
2666
3775
|
var TRANSPARENT_WRAPPERS = /* @__PURE__ */ new Set([
|
|
2667
3776
|
"sudo",
|
|
@@ -2688,7 +3797,7 @@ var INTERPRETER_SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
2688
3797
|
".sh"
|
|
2689
3798
|
]);
|
|
2690
3799
|
function normalizeHead(token) {
|
|
2691
|
-
const base =
|
|
3800
|
+
const base = path15.basename(token);
|
|
2692
3801
|
if (base && base !== "." && base !== "..") {
|
|
2693
3802
|
return base;
|
|
2694
3803
|
}
|
|
@@ -2793,7 +3902,7 @@ function isBareInterpreter(tokens) {
|
|
|
2793
3902
|
return false;
|
|
2794
3903
|
}
|
|
2795
3904
|
const scriptArg = args.find((token) => !token.startsWith("-"));
|
|
2796
|
-
if (scriptArg && INTERPRETER_SCRIPT_EXTENSIONS.has(
|
|
3905
|
+
if (scriptArg && INTERPRETER_SCRIPT_EXTENSIONS.has(path15.extname(scriptArg))) {
|
|
2797
3906
|
return false;
|
|
2798
3907
|
}
|
|
2799
3908
|
if (scriptArg) {
|
|
@@ -2861,7 +3970,7 @@ function redactCommand(command) {
|
|
|
2861
3970
|
return command.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]").replace(/sk-[A-Za-z0-9]{8,}/g, "sk-[REDACTED]").trim();
|
|
2862
3971
|
}
|
|
2863
3972
|
|
|
2864
|
-
// src/core/
|
|
3973
|
+
// src/core/verdict/verdict.ts
|
|
2865
3974
|
var DEFAULT_MAX_DEPTH = 8;
|
|
2866
3975
|
var TIER0_EXTERNAL_KEYS = /* @__PURE__ */ new Set([
|
|
2867
3976
|
"git push",
|
|
@@ -2944,6 +4053,7 @@ var LOCAL_ROUTINE_HEADS = /* @__PURE__ */ new Set([
|
|
|
2944
4053
|
"make",
|
|
2945
4054
|
"cmake"
|
|
2946
4055
|
]);
|
|
4056
|
+
var BELAY_SELF_COMMANDS = /* @__PURE__ */ new Set(["approve", "revoke"]);
|
|
2947
4057
|
var FIND_DANGEROUS_FLAGS = /* @__PURE__ */ new Set(["-delete", "-exec", "-execdir", "-ok", "-okdir"]);
|
|
2948
4058
|
function isFindDangerous(tokens) {
|
|
2949
4059
|
return tokens.some(
|
|
@@ -3091,8 +4201,36 @@ function tier0ExternalMatch(key, head, tokens) {
|
|
|
3091
4201
|
}
|
|
3092
4202
|
return false;
|
|
3093
4203
|
}
|
|
3094
|
-
function
|
|
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
|
+
}
|
|
4227
|
+
function isBelaySelfCommand(tokens) {
|
|
3095
4228
|
const head = tokens[0] ?? "";
|
|
4229
|
+
const subcommand = tokens[1] ?? "";
|
|
4230
|
+
return head === "belay" && (BELAY_SELF_COMMANDS.has(subcommand) || isBelayJudgeSelfCommand(tokens) || isBelayConfigSelfCommand(tokens));
|
|
4231
|
+
}
|
|
4232
|
+
function tier0HighStakesRm(tokens, context) {
|
|
4233
|
+
const head = (tokens[0] ?? "").split("/").pop() ?? "";
|
|
3096
4234
|
if (head !== "rm") {
|
|
3097
4235
|
return null;
|
|
3098
4236
|
}
|
|
@@ -3115,34 +4253,44 @@ function tier0HighStakesRm(tokens, context) {
|
|
|
3115
4253
|
signals: ["missing_trusted_cwd", ...analysis.signals]
|
|
3116
4254
|
});
|
|
3117
4255
|
}
|
|
3118
|
-
if (analysis.isHighStakes) {
|
|
3119
|
-
return askVerdict({
|
|
3120
|
-
location: analysis.location,
|
|
3121
|
-
opacity: "transparent",
|
|
3122
|
-
effect: "local_mutation",
|
|
3123
|
-
confidence: "deterministic",
|
|
3124
|
-
reason: "high_stakes_path",
|
|
3125
|
-
signals: ["high_stakes_path", ...analysis.signals]
|
|
3126
|
-
});
|
|
3127
|
-
}
|
|
3128
4256
|
for (const target of targets) {
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
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;
|
|
3144
4282
|
}
|
|
3145
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
|
+
}
|
|
3146
4294
|
}
|
|
3147
4295
|
return null;
|
|
3148
4296
|
}
|
|
@@ -3322,6 +4470,16 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3322
4470
|
if (rmVerdict) {
|
|
3323
4471
|
return rmVerdict;
|
|
3324
4472
|
}
|
|
4473
|
+
if (isBelaySelfCommand(peeled)) {
|
|
4474
|
+
return allowVerdict({
|
|
4475
|
+
location: "unknown",
|
|
4476
|
+
opacity: "transparent",
|
|
4477
|
+
effect: "local_mutation",
|
|
4478
|
+
confidence: "deterministic",
|
|
4479
|
+
reason: "belay_control_plane_command",
|
|
4480
|
+
signals: ["belay_control_plane_command", segment.head]
|
|
4481
|
+
});
|
|
4482
|
+
}
|
|
3325
4483
|
let effect = "unknown";
|
|
3326
4484
|
if (READ_ONLY_KEYS.has(segment.key) || READ_ONLY_KEYS.has(segment.head)) {
|
|
3327
4485
|
effect = "read_only";
|
|
@@ -3331,6 +4489,9 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3331
4489
|
effect = "local_mutation";
|
|
3332
4490
|
}
|
|
3333
4491
|
const pathArgs = extractPathArgs(peeled);
|
|
4492
|
+
if (extractRedirectTargets(peeled).length > 0 && effect === "read_only") {
|
|
4493
|
+
effect = "local_mutation";
|
|
4494
|
+
}
|
|
3334
4495
|
const pathAnalysis = analyzePathTargets({
|
|
3335
4496
|
targets: pathArgs,
|
|
3336
4497
|
cwd: context.cwd,
|
|
@@ -3361,15 +4522,46 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3361
4522
|
});
|
|
3362
4523
|
}
|
|
3363
4524
|
}
|
|
3364
|
-
if (
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
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
|
+
}
|
|
3373
4565
|
}
|
|
3374
4566
|
if (segment.head === "find" && isFindDangerous(peeled)) {
|
|
3375
4567
|
return askVerdict({
|
|
@@ -3381,15 +4573,14 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3381
4573
|
signals: ["find_dangerous_action"]
|
|
3382
4574
|
});
|
|
3383
4575
|
}
|
|
3384
|
-
if (pathAnalysis.location === "repo_outside" || pathAnalysis.location === "mixed") {
|
|
3385
|
-
|
|
3386
|
-
return askVerdict({
|
|
4576
|
+
if (effect === "read_only" && (pathAnalysis.location === "repo_outside" || pathAnalysis.location === "mixed")) {
|
|
4577
|
+
return allowVerdict({
|
|
3387
4578
|
location: pathAnalysis.location,
|
|
3388
|
-
opacity
|
|
3389
|
-
effect:
|
|
4579
|
+
opacity,
|
|
4580
|
+
effect: "read_only",
|
|
3390
4581
|
confidence: "deterministic",
|
|
3391
|
-
reason: "
|
|
3392
|
-
signals: ["
|
|
4582
|
+
reason: "read_only",
|
|
4583
|
+
signals: ["read_only", ...pathAnalysis.signals]
|
|
3393
4584
|
});
|
|
3394
4585
|
}
|
|
3395
4586
|
if (pathAnalysis.location === "unknown" && pathArgs.length > 0 && LOCAL_MUTATION_KEYS.has(segment.head)) {
|
|
@@ -3402,7 +4593,25 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3402
4593
|
signals: ["unknown_location_mutation"]
|
|
3403
4594
|
});
|
|
3404
4595
|
}
|
|
3405
|
-
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";
|
|
3406
4615
|
let tier1Trace;
|
|
3407
4616
|
if (needsTier1) {
|
|
3408
4617
|
const tier1Text = recursiveScript ?? command;
|
|
@@ -3414,9 +4623,9 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3414
4623
|
tier1Trace = context.judge.lastTrace;
|
|
3415
4624
|
if (tier1RequiresAsk(tier1)) {
|
|
3416
4625
|
return askVerdict({
|
|
3417
|
-
location: pathAnalysis.location === "unknown" ? "unknown" : "repo_local",
|
|
4626
|
+
location: pathAnalysis.location === "unknown" ? "unknown" : pathAnalysis.location === "repo_outside" || pathAnalysis.location === "mixed" ? pathAnalysis.location : "repo_local",
|
|
3418
4627
|
opacity,
|
|
3419
|
-
effect: tier1.
|
|
4628
|
+
effect: !tier1.local_recoverable ? "remote_mutation" : effect,
|
|
3420
4629
|
confidence: "llm",
|
|
3421
4630
|
reason: "tier1_catastrophic",
|
|
3422
4631
|
signals: ["tier1_catastrophic", tier1.reason],
|
|
@@ -3424,6 +4633,19 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3424
4633
|
});
|
|
3425
4634
|
}
|
|
3426
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
|
+
}
|
|
3427
4649
|
if (pathAnalysis.location === "repo_local" && (effect === "read_only" || effect === "local_mutation") && opacity !== "opaque") {
|
|
3428
4650
|
return withJudgeTrace(
|
|
3429
4651
|
allowVerdict({
|
|
@@ -3542,7 +4764,13 @@ async function verdict(command, context) {
|
|
|
3542
4764
|
);
|
|
3543
4765
|
}
|
|
3544
4766
|
|
|
3545
|
-
// src/core/
|
|
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
|
+
}
|
|
3546
4774
|
function buildVerdictContext(params) {
|
|
3547
4775
|
const protectedArtifactRoots2 = [
|
|
3548
4776
|
...params.options?.protectedArtifactRoots ?? [],
|
|
@@ -3551,7 +4779,7 @@ function buildVerdictContext(params) {
|
|
|
3551
4779
|
return {
|
|
3552
4780
|
cwd: params.cwd,
|
|
3553
4781
|
repoRoot: params.repoRoot,
|
|
3554
|
-
trustedCwd: params.
|
|
4782
|
+
trustedCwd: resolveClassifierTrustedCwd(params.cwd, params.options, params.trustedCwd),
|
|
3555
4783
|
sensitivePaths: params.options?.sensitivePaths ?? params.config.classifier.sensitivePaths,
|
|
3556
4784
|
protectedArtifactRoots: protectedArtifactRoots2.length > 0 ? [...new Set(protectedArtifactRoots2)] : void 0,
|
|
3557
4785
|
customAllowCommands: params.options?.customAllowCommands ?? params.config.overrides.allow,
|
|
@@ -3589,6 +4817,12 @@ function mapLegacyReason(result) {
|
|
|
3589
4817
|
if (result.reason === "repo_local_mutation") {
|
|
3590
4818
|
return "local_mutation";
|
|
3591
4819
|
}
|
|
4820
|
+
if (result.reason === "tier1_not_restorable") {
|
|
4821
|
+
return "tier1_catastrophic";
|
|
4822
|
+
}
|
|
4823
|
+
if (result.reason === "tier0_restorable" || result.reason === "tier1_restorable") {
|
|
4824
|
+
return result.effect === "local_mutation" ? "local_mutation" : result.reason;
|
|
4825
|
+
}
|
|
3592
4826
|
return result.reason;
|
|
3593
4827
|
}
|
|
3594
4828
|
function verdictToClassifyResult(result) {
|
|
@@ -3609,13 +4843,13 @@ function verdictToClassifyResult(result) {
|
|
|
3609
4843
|
assessment,
|
|
3610
4844
|
normalizedCommand: result.commandRedacted,
|
|
3611
4845
|
summary: result.commandRedacted,
|
|
3612
|
-
|
|
4846
|
+
axes: {
|
|
3613
4847
|
location: result.location,
|
|
3614
4848
|
opacity: result.opacity,
|
|
3615
4849
|
effect: result.effect,
|
|
3616
4850
|
confidence: result.confidence,
|
|
3617
4851
|
would: result.permission,
|
|
3618
|
-
by: "
|
|
4852
|
+
by: "verdict",
|
|
3619
4853
|
commandRedacted: result.commandRedacted,
|
|
3620
4854
|
commandFingerprint: result.fingerprint,
|
|
3621
4855
|
signals: result.signals,
|
|
@@ -3677,7 +4911,7 @@ function extractPatch(payload) {
|
|
|
3677
4911
|
}
|
|
3678
4912
|
return null;
|
|
3679
4913
|
}
|
|
3680
|
-
function
|
|
4914
|
+
function applyPatchTargets2(patch) {
|
|
3681
4915
|
const targets = [];
|
|
3682
4916
|
for (const line of patch.split("\n")) {
|
|
3683
4917
|
const match = line.match(/^\*\*\* (Add|Delete|Update) File: (.+)$/);
|
|
@@ -3695,6 +4929,70 @@ function applyPatchTargets(patch) {
|
|
|
3695
4929
|
function normalizedToolName(toolName) {
|
|
3696
4930
|
return toolName.trim().toLowerCase();
|
|
3697
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
|
+
}
|
|
3698
4996
|
async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
|
|
3699
4997
|
const toolName = String(payload.tool_name ?? "");
|
|
3700
4998
|
const toolKind = normalizedToolName(toolName);
|
|
@@ -3790,7 +5088,7 @@ async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
|
|
|
3790
5088
|
};
|
|
3791
5089
|
}
|
|
3792
5090
|
const signals = [];
|
|
3793
|
-
const resolvedPath =
|
|
5091
|
+
const resolvedPath = path16.isAbsolute(filePath) ? filePath : path16.resolve(cwd, filePath);
|
|
3794
5092
|
const hitsProtectedRoot = protectedRoots.some((root) => pathWithinRoot(root, resolvedPath));
|
|
3795
5093
|
if (hitsProtectedRoot) {
|
|
3796
5094
|
signals.push("control_plane_path");
|
|
@@ -3811,35 +5109,35 @@ async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
|
|
|
3811
5109
|
const relativePath = relativeWithinRepo(repoRoot, resolvedPath);
|
|
3812
5110
|
if (relativePath === null) {
|
|
3813
5111
|
signals.push("outside_repo_path");
|
|
3814
|
-
return {
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
};
|
|
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
|
+
});
|
|
3827
5125
|
}
|
|
3828
5126
|
if (matchesSensitivePath(relativePath, sensitivePaths)) {
|
|
3829
5127
|
signals.push("sensitive_path");
|
|
3830
|
-
return {
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
};
|
|
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
|
+
});
|
|
3843
5141
|
}
|
|
3844
5142
|
if (FILE_DELETE_TOOL_NAMES.has(toolKind)) {
|
|
3845
5143
|
signals.push("file_delete");
|
|
@@ -3874,7 +5172,7 @@ async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
|
|
|
3874
5172
|
}
|
|
3875
5173
|
if (APPLY_PATCH_TOOL_NAMES.has(toolKind)) {
|
|
3876
5174
|
const patch = extractPatch(payload);
|
|
3877
|
-
const targets = patch ?
|
|
5175
|
+
const targets = patch ? applyPatchTargets2(patch) : [];
|
|
3878
5176
|
if (targets.length === 0) {
|
|
3879
5177
|
if (options.unknownLocalEffect === "deny") {
|
|
3880
5178
|
return {
|
|
@@ -4064,10 +5362,56 @@ function normalizeGatedAction(params) {
|
|
|
4064
5362
|
agentAssessment
|
|
4065
5363
|
};
|
|
4066
5364
|
}
|
|
4067
|
-
function
|
|
4068
|
-
if (options.brokerFsScope
|
|
4069
|
-
|
|
4070
|
-
|
|
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") {
|
|
4071
5415
|
return {
|
|
4072
5416
|
...result,
|
|
4073
5417
|
verdict: "allow_flagged",
|
|
@@ -4085,6 +5429,38 @@ function applyShellPeripheralPolicy(command, action, result, options) {
|
|
|
4085
5429
|
}
|
|
4086
5430
|
return result;
|
|
4087
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
|
+
}
|
|
4088
5464
|
async function classifyGatedAction(action, config, extraOptions = {}) {
|
|
4089
5465
|
const options = { ...classifierOptionsFromConfig(config), ...extraOptions };
|
|
4090
5466
|
if (action.kind === "shell") {
|
|
@@ -4092,26 +5468,47 @@ async function classifyGatedAction(action, config, extraOptions = {}) {
|
|
|
4092
5468
|
if (!command) {
|
|
4093
5469
|
throw new GateNormalizationError("Shell gated action requires a command.");
|
|
4094
5470
|
}
|
|
4095
|
-
let
|
|
4096
|
-
|
|
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);
|
|
4097
5474
|
if (!action.agentAssessment) {
|
|
4098
|
-
return
|
|
5475
|
+
return result2;
|
|
4099
5476
|
}
|
|
4100
|
-
const
|
|
4101
|
-
if (!
|
|
4102
|
-
return { ...
|
|
5477
|
+
const merged2 = mergeAgentAssessment(result2.assessment, action.agentAssessment);
|
|
5478
|
+
if (!merged2.mismatch) {
|
|
5479
|
+
return { ...result2, assessment: merged2.assessment };
|
|
4103
5480
|
}
|
|
4104
5481
|
return {
|
|
4105
|
-
...
|
|
5482
|
+
...result2,
|
|
4106
5483
|
verdict: "deny_pending_approval",
|
|
4107
5484
|
reason: "agent_assessment_mismatch",
|
|
4108
|
-
assessment:
|
|
5485
|
+
assessment: merged2.assessment
|
|
4109
5486
|
};
|
|
4110
5487
|
}
|
|
4111
5488
|
if (action.kind === "subagent") {
|
|
4112
5489
|
return classifySubagent(action.payload ?? {}, action.repoRoot, options);
|
|
4113
5490
|
}
|
|
4114
|
-
|
|
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
|
+
};
|
|
4115
5512
|
}
|
|
4116
5513
|
async function classifyGatedActionAsync(action, config, extraOptions = {}) {
|
|
4117
5514
|
return classifyGatedAction(action, config, extraOptions);
|
|
@@ -4137,9 +5534,9 @@ function gateEnabledForAction(config, action) {
|
|
|
4137
5534
|
}
|
|
4138
5535
|
|
|
4139
5536
|
// src/core/transactional/diff-evaluator.ts
|
|
4140
|
-
import
|
|
5537
|
+
import path17 from "node:path";
|
|
4141
5538
|
function categorizeChange(change, ctx) {
|
|
4142
|
-
const absolutePath = canonicalPath(
|
|
5539
|
+
const absolutePath = canonicalPath(path17.join(ctx.repoRoot, change.relativePath));
|
|
4143
5540
|
if (!pathWithinRoot(ctx.repoRoot, absolutePath)) {
|
|
4144
5541
|
return "repo_outside";
|
|
4145
5542
|
}
|
|
@@ -4161,15 +5558,6 @@ function observedAssessment(evaluation) {
|
|
|
4161
5558
|
if (evaluation.deletedCount > 0) {
|
|
4162
5559
|
signals.push("observed_deletions");
|
|
4163
5560
|
}
|
|
4164
|
-
if (evaluation.categories.includes("repo_outside") || evaluation.categories.includes("control_plane") || evaluation.categories.includes("sensitive_path")) {
|
|
4165
|
-
return {
|
|
4166
|
-
reversibility: "irreversible",
|
|
4167
|
-
external: evaluation.categories.includes("repo_outside"),
|
|
4168
|
-
blastRadius: evaluation.categories.includes("control_plane") ? "agent-belay control plane" : evaluation.categories.includes("repo_outside") ? "outside the repository" : "sensitive path",
|
|
4169
|
-
confidence: 1,
|
|
4170
|
-
signals
|
|
4171
|
-
};
|
|
4172
|
-
}
|
|
4173
5561
|
if (evaluation.categories.includes("large_deletion")) {
|
|
4174
5562
|
return {
|
|
4175
5563
|
reversibility: "irreversible",
|
|
@@ -4197,7 +5585,7 @@ function evaluateTransactionalDiff(changes, ctx) {
|
|
|
4197
5585
|
categories.add("large_deletion");
|
|
4198
5586
|
}
|
|
4199
5587
|
const categoryList = [...categories];
|
|
4200
|
-
const dangerous = categoryList.includes("
|
|
5588
|
+
const dangerous = categoryList.includes("large_deletion");
|
|
4201
5589
|
const base = {
|
|
4202
5590
|
categories: categoryList,
|
|
4203
5591
|
changes,
|
|
@@ -4268,15 +5656,15 @@ var TRANSACTIONAL_APPROVAL_BYPASS_REASONS = /* @__PURE__ */ new Set([
|
|
|
4268
5656
|
]);
|
|
4269
5657
|
|
|
4270
5658
|
// src/core/transactional/git-worktree.ts
|
|
4271
|
-
import { spawn } from "node:child_process";
|
|
5659
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
4272
5660
|
import { randomUUID } from "node:crypto";
|
|
4273
5661
|
import { existsSync as existsSync5 } from "node:fs";
|
|
4274
|
-
import { copyFile, mkdir as
|
|
5662
|
+
import { copyFile, mkdir as mkdir3, mkdtemp, rm } from "node:fs/promises";
|
|
4275
5663
|
import os from "node:os";
|
|
4276
|
-
import
|
|
5664
|
+
import path18 from "node:path";
|
|
4277
5665
|
function execGit(repoRoot, args) {
|
|
4278
5666
|
return new Promise((resolve, reject) => {
|
|
4279
|
-
const child =
|
|
5667
|
+
const child = spawn2("git", ["-C", repoRoot, ...args], {
|
|
4280
5668
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4281
5669
|
});
|
|
4282
5670
|
const stdout = [];
|
|
@@ -4314,8 +5702,8 @@ async function isDirtyWorktree(repoRoot) {
|
|
|
4314
5702
|
}
|
|
4315
5703
|
}
|
|
4316
5704
|
async function createGitWorktreeSnapshot(repoRoot, stateDir) {
|
|
4317
|
-
const worktreePath =
|
|
4318
|
-
await
|
|
5705
|
+
const worktreePath = path18.join(stateDir, `tx-${randomUUID().replaceAll("-", "")}`);
|
|
5706
|
+
await mkdir3(stateDir, { recursive: true });
|
|
4319
5707
|
await execGit(repoRoot, ["worktree", "add", "--detach", worktreePath, "HEAD"]);
|
|
4320
5708
|
return {
|
|
4321
5709
|
worktreePath,
|
|
@@ -4334,18 +5722,18 @@ async function createGitWorktreeSnapshot(repoRoot, stateDir) {
|
|
|
4334
5722
|
}
|
|
4335
5723
|
function resolveWorktreeCwd(repoRoot, worktreePath, cwd) {
|
|
4336
5724
|
const resolvedCwd = canonicalPath(cwd);
|
|
4337
|
-
const relative =
|
|
4338
|
-
if (relative.startsWith("..") ||
|
|
5725
|
+
const relative = path18.relative(canonicalPath(repoRoot), resolvedCwd);
|
|
5726
|
+
if (relative.startsWith("..") || path18.isAbsolute(relative)) {
|
|
4339
5727
|
return worktreePath;
|
|
4340
5728
|
}
|
|
4341
5729
|
if (relative === "") {
|
|
4342
5730
|
return worktreePath;
|
|
4343
5731
|
}
|
|
4344
|
-
return
|
|
5732
|
+
return path18.join(worktreePath, relative);
|
|
4345
5733
|
}
|
|
4346
5734
|
function runShellCommand(command, cwd, timeoutMs) {
|
|
4347
5735
|
return new Promise((resolve) => {
|
|
4348
|
-
const child =
|
|
5736
|
+
const child = spawn2(command, {
|
|
4349
5737
|
cwd,
|
|
4350
5738
|
shell: true,
|
|
4351
5739
|
stdio: "ignore",
|
|
@@ -4411,7 +5799,7 @@ async function rollbackAppliedChanges(actions) {
|
|
|
4411
5799
|
for (const action of [...actions].reverse()) {
|
|
4412
5800
|
try {
|
|
4413
5801
|
if (action.type === "restore") {
|
|
4414
|
-
await
|
|
5802
|
+
await mkdir3(path18.dirname(action.target), { recursive: true });
|
|
4415
5803
|
await copyFile(action.backupPath, action.target);
|
|
4416
5804
|
} else {
|
|
4417
5805
|
await rm(action.target, { force: true });
|
|
@@ -4421,14 +5809,14 @@ async function rollbackAppliedChanges(actions) {
|
|
|
4421
5809
|
}
|
|
4422
5810
|
}
|
|
4423
5811
|
async function applyWorktreeChanges(worktreePath, repoRoot, changes) {
|
|
4424
|
-
const backupRoot = await mkdtemp(
|
|
5812
|
+
const backupRoot = await mkdtemp(path18.join(os.tmpdir(), "belay-tx-rollback-"));
|
|
4425
5813
|
const rollbackActions = [];
|
|
4426
5814
|
try {
|
|
4427
5815
|
for (const change of changes) {
|
|
4428
|
-
const target =
|
|
5816
|
+
const target = path18.join(repoRoot, change.relativePath);
|
|
4429
5817
|
if (existsSync5(target)) {
|
|
4430
|
-
const backupPath =
|
|
4431
|
-
await
|
|
5818
|
+
const backupPath = path18.join(backupRoot, change.relativePath);
|
|
5819
|
+
await mkdir3(path18.dirname(backupPath), { recursive: true });
|
|
4432
5820
|
await copyFile(target, backupPath);
|
|
4433
5821
|
rollbackActions.push({ type: "restore", target, backupPath });
|
|
4434
5822
|
} else if (change.kind !== "deleted") {
|
|
@@ -4438,8 +5826,8 @@ async function applyWorktreeChanges(worktreePath, repoRoot, changes) {
|
|
|
4438
5826
|
await rm(target, { force: true });
|
|
4439
5827
|
continue;
|
|
4440
5828
|
}
|
|
4441
|
-
const source =
|
|
4442
|
-
await
|
|
5829
|
+
const source = path18.join(worktreePath, change.relativePath);
|
|
5830
|
+
await mkdir3(path18.dirname(target), { recursive: true });
|
|
4443
5831
|
await copyFile(source, target);
|
|
4444
5832
|
}
|
|
4445
5833
|
} catch (error) {
|
|
@@ -4601,7 +5989,7 @@ async function notifyDeny(config, event) {
|
|
|
4601
5989
|
}
|
|
4602
5990
|
|
|
4603
5991
|
// src/adapters/layouts/protected-paths.ts
|
|
4604
|
-
import
|
|
5992
|
+
import path19 from "node:path";
|
|
4605
5993
|
function protectedArtifactRoots(layout, repoRoot, controlPlaneDir) {
|
|
4606
5994
|
const roots = [
|
|
4607
5995
|
layout.configPath(repoRoot),
|
|
@@ -4613,7 +6001,7 @@ function protectedArtifactRoots(layout, repoRoot, controlPlaneDir) {
|
|
|
4613
6001
|
if (controlPlaneDir) {
|
|
4614
6002
|
roots.push(controlPlaneDir);
|
|
4615
6003
|
}
|
|
4616
|
-
return roots.map((entry) =>
|
|
6004
|
+
return roots.map((entry) => path19.resolve(entry));
|
|
4617
6005
|
}
|
|
4618
6006
|
|
|
4619
6007
|
// src/adapters/shared/gate-runtime.ts
|
|
@@ -4623,7 +6011,7 @@ var EMPTY_APPROVALS = {
|
|
|
4623
6011
|
};
|
|
4624
6012
|
async function loadJsonFile(filePath, fallback) {
|
|
4625
6013
|
try {
|
|
4626
|
-
const raw = await
|
|
6014
|
+
const raw = await readFile3(filePath, "utf8");
|
|
4627
6015
|
return JSON.parse(raw);
|
|
4628
6016
|
} catch {
|
|
4629
6017
|
return fallback;
|
|
@@ -4635,14 +6023,14 @@ function createDefaultGateRuntimeDeps() {
|
|
|
4635
6023
|
return loadJsonFile(configPath, {});
|
|
4636
6024
|
},
|
|
4637
6025
|
async appendAudit(ctx, event) {
|
|
4638
|
-
const auditPath =
|
|
4639
|
-
await
|
|
6026
|
+
const auditPath = path20.join(ctx.repoRoot, ctx.config.audit.logPath);
|
|
6027
|
+
await mkdir4(path20.dirname(auditPath), { recursive: true });
|
|
4640
6028
|
const record = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), ...event };
|
|
4641
6029
|
if (!ctx.config.audit.includeAssessment) {
|
|
4642
6030
|
delete record.assessment;
|
|
4643
6031
|
}
|
|
4644
6032
|
const scrubbed = scrubValue(record, scrubOptionsFromConfig(ctx.config));
|
|
4645
|
-
await
|
|
6033
|
+
await writeFile3(auditPath, `${JSON.stringify(scrubbed)}
|
|
4646
6034
|
`, {
|
|
4647
6035
|
encoding: "utf8",
|
|
4648
6036
|
flag: "a"
|
|
@@ -4661,8 +6049,8 @@ function createDefaultGateRuntimeDeps() {
|
|
|
4661
6049
|
};
|
|
4662
6050
|
},
|
|
4663
6051
|
async writeApprovals(filePath, state) {
|
|
4664
|
-
await
|
|
4665
|
-
await
|
|
6052
|
+
await mkdir4(path20.dirname(filePath), { recursive: true });
|
|
6053
|
+
await writeFile3(filePath, `${JSON.stringify(compactApprovals(state), null, 2)}
|
|
4666
6054
|
`, "utf8");
|
|
4667
6055
|
}
|
|
4668
6056
|
};
|
|
@@ -4672,7 +6060,7 @@ async function resolveGateConfig(ctx, deps) {
|
|
|
4672
6060
|
let teamConfig = null;
|
|
4673
6061
|
const teamPath = teamConfigPath();
|
|
4674
6062
|
if (existsSync6(teamPath)) {
|
|
4675
|
-
teamConfig = JSON.parse(await
|
|
6063
|
+
teamConfig = JSON.parse(await readFile3(teamPath, "utf8"));
|
|
4676
6064
|
}
|
|
4677
6065
|
return resolveLayeredConfig({
|
|
4678
6066
|
repoConfig: loaded,
|
|
@@ -4744,7 +6132,15 @@ async function consumeApprovedApproval(ctx, deps, kind, fingerprint) {
|
|
|
4744
6132
|
await deps.writeApprovals(approved.filePath, approved.state);
|
|
4745
6133
|
return null;
|
|
4746
6134
|
}
|
|
4747
|
-
const
|
|
6135
|
+
const approval = approved.state.approvals[index];
|
|
6136
|
+
if (approval.executionLeaseExpiresAt) {
|
|
6137
|
+
await deps.writeApprovals(approved.filePath, approved.state);
|
|
6138
|
+
return approval;
|
|
6139
|
+
}
|
|
6140
|
+
approved.state.approvals[index] = {
|
|
6141
|
+
...approval,
|
|
6142
|
+
executionLeaseExpiresAt: new Date(Date.now() + APPROVAL_EXECUTION_LEASE_MS).toISOString()
|
|
6143
|
+
};
|
|
4748
6144
|
await deps.writeApprovals(approved.filePath, approved.state);
|
|
4749
6145
|
return approval;
|
|
4750
6146
|
}
|
|
@@ -4809,7 +6205,7 @@ async function evaluateGatedAction(ctx, deps, params) {
|
|
|
4809
6205
|
command: params.command,
|
|
4810
6206
|
cwd: params.cwd,
|
|
4811
6207
|
repoRoot: ctx.repoRoot,
|
|
4812
|
-
stateDir:
|
|
6208
|
+
stateDir: path20.join(ctx.layout.repoLocalStateDir(ctx.repoRoot), "transactional"),
|
|
4813
6209
|
timeoutMs: transactional.timeoutMs,
|
|
4814
6210
|
predicted,
|
|
4815
6211
|
diffContext: {
|
|
@@ -4882,8 +6278,8 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
4882
6278
|
predictedAssessment: auditExtras.predictedAssessment,
|
|
4883
6279
|
observedAssessment: auditExtras.observedAssessment,
|
|
4884
6280
|
mode: ctx.config.mode,
|
|
4885
|
-
schemaVersion: result.
|
|
4886
|
-
...result.
|
|
6281
|
+
schemaVersion: result.axes ? 2 : 1,
|
|
6282
|
+
...result.axes ?? {},
|
|
4887
6283
|
...auditExtras.transactionalLayer
|
|
4888
6284
|
};
|
|
4889
6285
|
if (result.reason === TRANSACTIONAL_ALREADY_APPLIED) {
|
|
@@ -4906,7 +6302,17 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
4906
6302
|
});
|
|
4907
6303
|
}
|
|
4908
6304
|
const brokerActive = isCapabilityBrokerDemotionActive(ctx.config);
|
|
4909
|
-
|
|
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
|
+
}
|
|
4910
6316
|
if (approved) {
|
|
4911
6317
|
await deps.appendAudit(ctx, {
|
|
4912
6318
|
...gateBase,
|
|
@@ -5084,18 +6490,18 @@ async function appendObservedAudit(ctx, deps, eventName, payload) {
|
|
|
5084
6490
|
|
|
5085
6491
|
// src/adapters/shared/repo-root.ts
|
|
5086
6492
|
import { existsSync as existsSync7 } from "node:fs";
|
|
5087
|
-
import
|
|
6493
|
+
import path21 from "node:path";
|
|
5088
6494
|
function findRepoRoot(startPath, layout) {
|
|
5089
|
-
let current =
|
|
6495
|
+
let current = path21.resolve(startPath);
|
|
5090
6496
|
while (true) {
|
|
5091
6497
|
for (const marker of layout.repoRootMarkers) {
|
|
5092
|
-
if (existsSync7(
|
|
6498
|
+
if (existsSync7(path21.join(current, marker))) {
|
|
5093
6499
|
return current;
|
|
5094
6500
|
}
|
|
5095
6501
|
}
|
|
5096
|
-
const parent =
|
|
6502
|
+
const parent = path21.dirname(current);
|
|
5097
6503
|
if (parent === current) {
|
|
5098
|
-
return
|
|
6504
|
+
return path21.resolve(startPath);
|
|
5099
6505
|
}
|
|
5100
6506
|
current = parent;
|
|
5101
6507
|
}
|