@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 claude 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/claude/runtime-entry.ts
|
|
35
4
|
import process2 from "node:process";
|
|
@@ -52,7 +21,7 @@ function classifyResultToGateVerdict(params) {
|
|
|
52
21
|
approvalId,
|
|
53
22
|
user_message,
|
|
54
23
|
agent_message,
|
|
55
|
-
|
|
24
|
+
axes: result.axes
|
|
56
25
|
};
|
|
57
26
|
}
|
|
58
27
|
function unnormalizedGateVerdict(params) {
|
|
@@ -82,12 +51,158 @@ import path3 from "node:path";
|
|
|
82
51
|
// src/core/config.ts
|
|
83
52
|
import path from "node:path";
|
|
84
53
|
|
|
54
|
+
// src/core/judge-model-policy.ts
|
|
55
|
+
var DEPRECATED_JUDGE_MODEL_AUTO = "auto";
|
|
56
|
+
function isDeprecatedJudgeModelAuto(model) {
|
|
57
|
+
return model?.trim().toLowerCase() === DEPRECATED_JUDGE_MODEL_AUTO;
|
|
58
|
+
}
|
|
59
|
+
function rejectDeprecatedJudgeModelAuto(model) {
|
|
60
|
+
if (isDeprecatedJudgeModelAuto(model)) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
'judge model "auto" is no longer accepted. Use a concrete model id from belay judge list or the provider catalog default.'
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
var warnedDeprecatedAuto = false;
|
|
67
|
+
function warnDeprecatedJudgeModelAuto() {
|
|
68
|
+
if (warnedDeprecatedAuto) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
warnedDeprecatedAuto = true;
|
|
72
|
+
process.stderr.write(
|
|
73
|
+
'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'
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/core/verdict/judge-catalog.ts
|
|
78
|
+
var LEGACY_PROVIDER_ID_ALIASES = {
|
|
79
|
+
local: "ollama",
|
|
80
|
+
openai: "codex"
|
|
81
|
+
};
|
|
82
|
+
var REMOVED_JUDGE_PROVIDER_IDS = ["openrouter", "custom"];
|
|
83
|
+
function isRemovedProviderId(id) {
|
|
84
|
+
return REMOVED_JUDGE_PROVIDER_IDS.includes(id);
|
|
85
|
+
}
|
|
86
|
+
function warnRemovedProviderId(id) {
|
|
87
|
+
process.stderr.write(
|
|
88
|
+
`Warning: judge.providerId "${id}" was removed; migrate with belay config set judge.providerId <ollama|codex|claude|cursor>.
|
|
89
|
+
`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
var JUDGE_CATALOG = {
|
|
93
|
+
ollama: {
|
|
94
|
+
id: "ollama",
|
|
95
|
+
driver: "ollama",
|
|
96
|
+
defaultEndpoint: "http://localhost:11434",
|
|
97
|
+
defaultModel: "gemma4:e2b",
|
|
98
|
+
apiKeyEnvVars: [],
|
|
99
|
+
isCloud: false
|
|
100
|
+
},
|
|
101
|
+
codex: {
|
|
102
|
+
id: "codex",
|
|
103
|
+
driver: "openai-compatible",
|
|
104
|
+
defaultEndpoint: null,
|
|
105
|
+
defaultModel: "gpt-5.3-codex-high",
|
|
106
|
+
apiKeyEnvVars: ["OPENAI_API_KEY", "BELAY_JUDGE_API_KEY"],
|
|
107
|
+
isCloud: true
|
|
108
|
+
},
|
|
109
|
+
claude: {
|
|
110
|
+
id: "claude",
|
|
111
|
+
driver: "anthropic",
|
|
112
|
+
defaultEndpoint: null,
|
|
113
|
+
defaultModel: "claude-sonnet-4-6",
|
|
114
|
+
apiKeyEnvVars: ["ANTHROPIC_API_KEY", "BELAY_JUDGE_API_KEY"],
|
|
115
|
+
isCloud: true
|
|
116
|
+
},
|
|
117
|
+
cursor: {
|
|
118
|
+
id: "cursor",
|
|
119
|
+
driver: "openai-compatible",
|
|
120
|
+
defaultEndpoint: null,
|
|
121
|
+
defaultModel: "composer-2.5",
|
|
122
|
+
apiKeyEnvVars: ["CURSOR_API_KEY", "BELAY_JUDGE_API_KEY"],
|
|
123
|
+
isCloud: true
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
var JUDGE_PROVIDER_IDS = Object.keys(JUDGE_CATALOG);
|
|
127
|
+
function normalizeLegacyProviderId(id) {
|
|
128
|
+
if (id in JUDGE_CATALOG) {
|
|
129
|
+
return id;
|
|
130
|
+
}
|
|
131
|
+
return LEGACY_PROVIDER_ID_ALIASES[id] ?? null;
|
|
132
|
+
}
|
|
133
|
+
function getJudgeProviderSpec(id) {
|
|
134
|
+
const normalized = normalizeLegacyProviderId(id);
|
|
135
|
+
if (!normalized) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
return JUDGE_CATALOG[normalized];
|
|
139
|
+
}
|
|
140
|
+
function inferProviderIdFromConfig(judge) {
|
|
141
|
+
if (judge.providerId) {
|
|
142
|
+
const normalized = normalizeLegacyProviderId(judge.providerId);
|
|
143
|
+
if (normalized) {
|
|
144
|
+
return normalized;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (judge.provider === "ollama") {
|
|
148
|
+
return "ollama";
|
|
149
|
+
}
|
|
150
|
+
if (judge.provider === "anthropic") {
|
|
151
|
+
return "claude";
|
|
152
|
+
}
|
|
153
|
+
const endpoint = judge.endpoint?.trim().toLowerCase() ?? "";
|
|
154
|
+
if (endpoint.includes("openrouter.ai")) {
|
|
155
|
+
return "codex";
|
|
156
|
+
}
|
|
157
|
+
if (endpoint.includes("api.openai.com")) {
|
|
158
|
+
return "codex";
|
|
159
|
+
}
|
|
160
|
+
if (endpoint) {
|
|
161
|
+
return "codex";
|
|
162
|
+
}
|
|
163
|
+
if (judge.provider === "openai-compatible") {
|
|
164
|
+
const model = judge.model?.trim().toLowerCase() ?? "";
|
|
165
|
+
if (model.includes("composer")) {
|
|
166
|
+
return "cursor";
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return "codex";
|
|
170
|
+
}
|
|
171
|
+
function resolveJudgeFromCatalog(input) {
|
|
172
|
+
const providerId = normalizeLegacyProviderId(String(input.providerId));
|
|
173
|
+
if (!providerId) {
|
|
174
|
+
throw new Error(`Unknown judge provider id: ${input.providerId}`);
|
|
175
|
+
}
|
|
176
|
+
const spec = JUDGE_CATALOG[providerId];
|
|
177
|
+
const model = input.model?.trim() || spec.defaultModel;
|
|
178
|
+
const endpoint = input.endpoint !== void 0 ? input.endpoint?.trim() || null : spec.defaultEndpoint;
|
|
179
|
+
return {
|
|
180
|
+
provider: spec.driver,
|
|
181
|
+
providerId: spec.id,
|
|
182
|
+
model,
|
|
183
|
+
endpoint,
|
|
184
|
+
timeoutMs: input.timeoutMs ?? (spec.isCloud ? 8e3 : 25e3),
|
|
185
|
+
keepAlive: spec.driver === "ollama" ? input.keepAlive ?? "30m" : null
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function isCloudProviderId(providerId) {
|
|
189
|
+
if (!providerId) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
const normalized = normalizeLegacyProviderId(providerId);
|
|
193
|
+
if (!normalized) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
return JUDGE_CATALOG[normalized].isCloud;
|
|
197
|
+
}
|
|
198
|
+
|
|
85
199
|
// src/core/audit-summary.ts
|
|
86
200
|
var DEFAULT_SILENT_PASS_THRESHOLD = 0.5;
|
|
87
201
|
|
|
88
202
|
// src/core/config.ts
|
|
89
203
|
var DEFAULT_JUDGE_LOCAL_OLLAMA = {
|
|
90
204
|
provider: "ollama",
|
|
205
|
+
providerId: "ollama",
|
|
91
206
|
model: "gemma4:e2b",
|
|
92
207
|
endpoint: "http://localhost:11434",
|
|
93
208
|
timeoutMs: 25e3,
|
|
@@ -95,7 +210,8 @@ var DEFAULT_JUDGE_LOCAL_OLLAMA = {
|
|
|
95
210
|
};
|
|
96
211
|
var DEFAULT_JUDGE_OPENAI_COMPATIBLE_TEMPLATE = {
|
|
97
212
|
provider: "openai-compatible",
|
|
98
|
-
|
|
213
|
+
providerId: "codex",
|
|
214
|
+
model: "gpt-5.3-codex-high",
|
|
99
215
|
timeoutMs: 8e3,
|
|
100
216
|
endpoint: null,
|
|
101
217
|
keepAlive: null
|
|
@@ -128,7 +244,7 @@ var LEGACY_POLICY_V3 = {
|
|
|
128
244
|
fenceWarnThreshold: DEFAULT_FENCE_WARN_THRESHOLD
|
|
129
245
|
};
|
|
130
246
|
var DEFAULT_POLICY_V3 = {
|
|
131
|
-
unknownLocalEffect: "
|
|
247
|
+
unknownLocalEffect: "allow_flagged",
|
|
132
248
|
unparseableShell: "deny",
|
|
133
249
|
codexUnmappedTool: "deny",
|
|
134
250
|
confidenceThresholds: { ...DEFAULT_CONFIDENCE_THRESHOLDS },
|
|
@@ -271,36 +387,125 @@ function migrateV2ToV3(v2, rawOverrides) {
|
|
|
271
387
|
});
|
|
272
388
|
}
|
|
273
389
|
function normalizeJudgeProvider(provider) {
|
|
390
|
+
if (provider === "anthropic") {
|
|
391
|
+
return "anthropic";
|
|
392
|
+
}
|
|
274
393
|
if (provider === "openai-compatible" || provider === "cursor") {
|
|
275
394
|
return "openai-compatible";
|
|
276
395
|
}
|
|
277
396
|
return "ollama";
|
|
278
397
|
}
|
|
398
|
+
function defaultJudgeTemplateForProvider(provider) {
|
|
399
|
+
if (provider === "ollama") {
|
|
400
|
+
return DEFAULT_JUDGE_LOCAL_OLLAMA;
|
|
401
|
+
}
|
|
402
|
+
if (provider === "anthropic") {
|
|
403
|
+
return {
|
|
404
|
+
provider: "anthropic",
|
|
405
|
+
providerId: "claude",
|
|
406
|
+
model: "claude-sonnet-4-6",
|
|
407
|
+
timeoutMs: 8e3,
|
|
408
|
+
endpoint: null,
|
|
409
|
+
keepAlive: null
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
return DEFAULT_JUDGE_OPENAI_COMPATIBLE_TEMPLATE;
|
|
413
|
+
}
|
|
279
414
|
function synthesizeJudgeFromRaw(raw) {
|
|
280
415
|
const judge = raw.judge;
|
|
281
416
|
if (judge?.provider) {
|
|
282
|
-
const
|
|
283
|
-
const
|
|
417
|
+
const rawProvider = String(judge.provider);
|
|
418
|
+
const provider = normalizeJudgeProvider(rawProvider);
|
|
419
|
+
const base = defaultJudgeTemplateForProvider(provider);
|
|
420
|
+
const providerId = judge.providerId && normalizeLegacyProviderId(judge.providerId) ? normalizeLegacyProviderId(judge.providerId) : rawProvider === "cursor" ? "cursor" : void 0;
|
|
284
421
|
return normalizeJudgeConfig({
|
|
285
422
|
...base,
|
|
286
423
|
...judge,
|
|
287
|
-
provider
|
|
424
|
+
provider,
|
|
425
|
+
...providerId ? { providerId } : {}
|
|
288
426
|
});
|
|
289
427
|
}
|
|
290
428
|
return { ...DEFAULT_JUDGE_LOCAL_OLLAMA };
|
|
291
429
|
}
|
|
292
430
|
function normalizeJudgeConfig(judge) {
|
|
293
431
|
const provider = normalizeJudgeProvider(judge.provider);
|
|
294
|
-
const base = provider
|
|
295
|
-
const
|
|
432
|
+
const base = defaultJudgeTemplateForProvider(provider);
|
|
433
|
+
const rawProviderId = judge.providerId ? String(judge.providerId) : void 0;
|
|
434
|
+
if (rawProviderId && isRemovedProviderId(rawProviderId)) {
|
|
435
|
+
warnRemovedProviderId(rawProviderId);
|
|
436
|
+
let model2 = typeof judge.model === "string" && judge.model.trim() ? judge.model.trim() : base.model;
|
|
437
|
+
if (model2 === "auto") {
|
|
438
|
+
warnDeprecatedJudgeModelAuto();
|
|
439
|
+
model2 = base.model;
|
|
440
|
+
}
|
|
441
|
+
const timeoutMs2 = typeof judge.timeoutMs === "number" && judge.timeoutMs > 0 ? judge.timeoutMs : base.timeoutMs;
|
|
442
|
+
const endpoint2 = typeof judge.endpoint === "string" && judge.endpoint.trim() ? judge.endpoint.trim() : null;
|
|
443
|
+
return {
|
|
444
|
+
provider,
|
|
445
|
+
providerId: rawProviderId,
|
|
446
|
+
model: model2,
|
|
447
|
+
timeoutMs: timeoutMs2,
|
|
448
|
+
endpoint: endpoint2,
|
|
449
|
+
keepAlive: provider === "ollama" && typeof judge.keepAlive === "string" && judge.keepAlive.trim() ? judge.keepAlive.trim() : provider === "ollama" ? DEFAULT_JUDGE_LOCAL_OLLAMA.keepAlive : null,
|
|
450
|
+
...judge.cloudConsent ? { cloudConsent: judge.cloudConsent } : {},
|
|
451
|
+
...judge.credential ? { credential: judge.credential } : {}
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
let providerId = rawProviderId && normalizeLegacyProviderId(rawProviderId) ? normalizeLegacyProviderId(rawProviderId) : inferProviderIdFromConfig({ ...judge, provider });
|
|
455
|
+
const spec = getJudgeProviderSpec(providerId);
|
|
456
|
+
if (spec && spec.driver !== provider) {
|
|
457
|
+
providerId = inferProviderIdFromConfig({ ...judge, provider });
|
|
458
|
+
}
|
|
459
|
+
const catalogSpec = getJudgeProviderSpec(providerId);
|
|
460
|
+
let model = typeof judge.model === "string" && judge.model.trim() ? judge.model.trim() : base.model;
|
|
461
|
+
if (model === "auto" && catalogSpec?.defaultModel) {
|
|
462
|
+
warnDeprecatedJudgeModelAuto();
|
|
463
|
+
model = catalogSpec.defaultModel;
|
|
464
|
+
}
|
|
465
|
+
if (!model && catalogSpec?.defaultModel) {
|
|
466
|
+
model = catalogSpec.defaultModel;
|
|
467
|
+
}
|
|
296
468
|
const timeoutMs = typeof judge.timeoutMs === "number" && judge.timeoutMs > 0 ? judge.timeoutMs : base.timeoutMs;
|
|
297
|
-
|
|
469
|
+
let endpoint = typeof judge.endpoint === "string" && judge.endpoint.trim() ? judge.endpoint.trim() : null;
|
|
470
|
+
if (!endpoint && catalogSpec?.defaultEndpoint) {
|
|
471
|
+
endpoint = catalogSpec.defaultEndpoint;
|
|
472
|
+
}
|
|
473
|
+
const normalized = {
|
|
298
474
|
provider,
|
|
475
|
+
providerId,
|
|
299
476
|
model,
|
|
300
477
|
timeoutMs,
|
|
301
|
-
endpoint
|
|
478
|
+
endpoint,
|
|
302
479
|
keepAlive: provider === "ollama" && typeof judge.keepAlive === "string" && judge.keepAlive.trim() ? judge.keepAlive.trim() : provider === "ollama" ? DEFAULT_JUDGE_LOCAL_OLLAMA.keepAlive : null
|
|
303
480
|
};
|
|
481
|
+
if (judge.cloudConsent?.accepted) {
|
|
482
|
+
normalized.cloudConsent = {
|
|
483
|
+
accepted: true,
|
|
484
|
+
at: judge.cloudConsent.at,
|
|
485
|
+
providerId: judge.cloudConsent.providerId ?? providerId,
|
|
486
|
+
endpoint: judge.cloudConsent.endpoint,
|
|
487
|
+
by: judge.cloudConsent.by
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
if (judge.credential?.mode === "project" || judge.credential?.mode === "apiKey") {
|
|
491
|
+
normalized.credential = {
|
|
492
|
+
mode: judge.credential.mode,
|
|
493
|
+
...judge.credential.ref ? { ref: judge.credential.ref } : {}
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
return normalized;
|
|
497
|
+
}
|
|
498
|
+
function rejectTeamLayerJudgeSecrets(judge, source) {
|
|
499
|
+
if (source !== "team" || !judge) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (judge.credential?.mode === "apiKey") {
|
|
503
|
+
throw new Error("team config cannot set judge.credential.mode to apiKey.");
|
|
504
|
+
}
|
|
505
|
+
const raw = judge;
|
|
506
|
+
if (raw.credential && "key" in raw.credential && raw.credential.key) {
|
|
507
|
+
throw new Error("team config cannot contain inline judge credential keys.");
|
|
508
|
+
}
|
|
304
509
|
}
|
|
305
510
|
function hasV3Sections(raw) {
|
|
306
511
|
return raw.policy !== void 0 || raw.overrides !== void 0 || raw.redaction !== void 0 || raw.controlPlane !== void 0;
|
|
@@ -741,20 +946,29 @@ var claudeLayout = {
|
|
|
741
946
|
// src/adapters/shared/gate-runtime.ts
|
|
742
947
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
743
948
|
import { existsSync as existsSync6 } from "node:fs";
|
|
744
|
-
import { mkdir as
|
|
745
|
-
import
|
|
949
|
+
import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
950
|
+
import path20 from "node:path";
|
|
746
951
|
|
|
747
952
|
// src/core/approval.ts
|
|
953
|
+
var APPROVAL_EXECUTION_LEASE_MS = 6e4;
|
|
748
954
|
function nowIso() {
|
|
749
955
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
750
956
|
}
|
|
751
957
|
function isExpired(approval) {
|
|
752
958
|
return Date.parse(approval.expiresAt) <= Date.now();
|
|
753
959
|
}
|
|
960
|
+
function isExecutionLeaseExpired(approval) {
|
|
961
|
+
if (!approval.executionLeaseExpiresAt) {
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
964
|
+
return Date.parse(approval.executionLeaseExpiresAt) <= Date.now();
|
|
965
|
+
}
|
|
754
966
|
function compactApprovals(state) {
|
|
755
967
|
return {
|
|
756
968
|
version: state.version,
|
|
757
|
-
approvals: state.approvals.filter(
|
|
969
|
+
approvals: state.approvals.filter(
|
|
970
|
+
(approval) => !isExpired(approval) && !isExecutionLeaseExpired(approval)
|
|
971
|
+
)
|
|
758
972
|
};
|
|
759
973
|
}
|
|
760
974
|
function escapeRegex(value) {
|
|
@@ -763,8 +977,15 @@ function escapeRegex(value) {
|
|
|
763
977
|
}
|
|
764
978
|
function approvalCommandMatch(prompt, tokenPrefix) {
|
|
765
979
|
const escapedPrefix = escapeRegex(tokenPrefix);
|
|
766
|
-
const
|
|
767
|
-
|
|
980
|
+
const linePattern = new RegExp(`^\\s*${escapedPrefix}\\s+(\\S+)\\s*$`, "i");
|
|
981
|
+
for (const line of prompt.split(/\r?\n/)) {
|
|
982
|
+
if (!line.trim()) {
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
const match = line.match(linePattern);
|
|
986
|
+
return match?.[1] ?? null;
|
|
987
|
+
}
|
|
988
|
+
return null;
|
|
768
989
|
}
|
|
769
990
|
function buildRetryInstruction(tokenPrefix, approvalId) {
|
|
770
991
|
return `To allow the next matching action once, send ${tokenPrefix} ${approvalId} and then retry the original action unchanged.`;
|
|
@@ -997,6 +1218,9 @@ function isCapabilityBrokerDemotionActive(config) {
|
|
|
997
1218
|
return hasSandboxRuntime(config);
|
|
998
1219
|
}
|
|
999
1220
|
|
|
1221
|
+
// src/core/capability/paths.ts
|
|
1222
|
+
import path7 from "node:path";
|
|
1223
|
+
|
|
1000
1224
|
// src/core/shell-tokenizer.ts
|
|
1001
1225
|
function tokenizeShell(input) {
|
|
1002
1226
|
const tokens = [];
|
|
@@ -1094,33 +1318,106 @@ function extractRedirectTargets(tokens) {
|
|
|
1094
1318
|
}
|
|
1095
1319
|
|
|
1096
1320
|
// src/core/capability/paths.ts
|
|
1321
|
+
function applyPatchTargets(patch) {
|
|
1322
|
+
const targets = [];
|
|
1323
|
+
for (const line of patch.split("\n")) {
|
|
1324
|
+
const match = line.match(/^\*\*\* (?:Add|Delete|Update) File: (.+)$/);
|
|
1325
|
+
if (match?.[1]) {
|
|
1326
|
+
targets.push(match[1]);
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
const moveMatch = line.match(/^\*\*\* Move to: (.+)$/);
|
|
1330
|
+
if (moveMatch?.[1]) {
|
|
1331
|
+
targets.push(moveMatch[1]);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
return targets;
|
|
1335
|
+
}
|
|
1336
|
+
function extractToolFilePath(payload) {
|
|
1337
|
+
const toolInput = payload.tool_input;
|
|
1338
|
+
if (!toolInput || typeof toolInput !== "object") {
|
|
1339
|
+
return null;
|
|
1340
|
+
}
|
|
1341
|
+
const input = toolInput;
|
|
1342
|
+
for (const key of ["path", "file_path", "target_file", "filePath"]) {
|
|
1343
|
+
if (typeof input[key] === "string") {
|
|
1344
|
+
return input[key];
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
return null;
|
|
1348
|
+
}
|
|
1349
|
+
function extractToolPatch(payload) {
|
|
1350
|
+
const toolInput = payload.tool_input;
|
|
1351
|
+
if (!toolInput || typeof toolInput !== "object") {
|
|
1352
|
+
return null;
|
|
1353
|
+
}
|
|
1354
|
+
const input = toolInput;
|
|
1355
|
+
for (const key of ["patch", "input", "text"]) {
|
|
1356
|
+
if (typeof input[key] === "string" && input[key].trim()) {
|
|
1357
|
+
return input[key];
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
return null;
|
|
1361
|
+
}
|
|
1362
|
+
function addOutsideRepoPath(paths, token, cwd, repoRoot) {
|
|
1363
|
+
const resolved = resolveMutationTarget(token, cwd);
|
|
1364
|
+
if (resolved && relativeWithinRepo(repoRoot, resolved) === null) {
|
|
1365
|
+
paths.add(resolved);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1097
1368
|
function collectOutsideRepoPaths(command, cwd, repoRoot) {
|
|
1098
1369
|
const tokens = tokenizeShell(command);
|
|
1099
1370
|
const redirects = extractRedirectTargets(tokens);
|
|
1100
1371
|
const paths = /* @__PURE__ */ new Set();
|
|
1101
1372
|
for (const token of tokens.slice(1)) {
|
|
1102
|
-
|
|
1103
|
-
if (resolved && relativeWithinRepo(repoRoot, resolved) === null) {
|
|
1104
|
-
paths.add(resolved);
|
|
1105
|
-
}
|
|
1373
|
+
addOutsideRepoPath(paths, token, cwd, repoRoot);
|
|
1106
1374
|
}
|
|
1107
1375
|
for (const redirect of redirects) {
|
|
1108
|
-
|
|
1109
|
-
|
|
1376
|
+
addOutsideRepoPath(paths, redirect, cwd, repoRoot);
|
|
1377
|
+
}
|
|
1378
|
+
return [...paths];
|
|
1379
|
+
}
|
|
1380
|
+
function collectOutsideRepoPathsFromToolPayload(payload, cwd, repoRoot) {
|
|
1381
|
+
const toolKind = String(payload.tool_name ?? "").trim().toLowerCase();
|
|
1382
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1383
|
+
const filePath = extractToolFilePath(payload);
|
|
1384
|
+
if (filePath) {
|
|
1385
|
+
const resolved = path7.isAbsolute(filePath) ? filePath : path7.resolve(cwd, filePath);
|
|
1386
|
+
if (relativeWithinRepo(repoRoot, resolved) === null) {
|
|
1110
1387
|
paths.add(resolved);
|
|
1111
1388
|
}
|
|
1389
|
+
return [...paths];
|
|
1390
|
+
}
|
|
1391
|
+
if (toolKind === "apply_patch" || toolKind === "applypatch") {
|
|
1392
|
+
const patch = extractToolPatch(payload);
|
|
1393
|
+
if (patch) {
|
|
1394
|
+
for (const target of applyPatchTargets(patch)) {
|
|
1395
|
+
const resolved = path7.isAbsolute(target) ? target : path7.resolve(cwd, target);
|
|
1396
|
+
if (relativeWithinRepo(repoRoot, resolved) === null) {
|
|
1397
|
+
paths.add(resolved);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1112
1401
|
}
|
|
1113
1402
|
return [...paths];
|
|
1114
1403
|
}
|
|
1115
1404
|
|
|
1116
1405
|
// src/core/capability/reasons.ts
|
|
1117
1406
|
var FS_SCOPE_REASONS = /* @__PURE__ */ new Set(["outside_repo_mutation", "outside_repo_redirect"]);
|
|
1407
|
+
var JUDGE_CLOUD_CONSENT_REASON = "judge_cloud_consent";
|
|
1408
|
+
var CAPABILITY_APPROVAL_REASONS = /* @__PURE__ */ new Set([
|
|
1409
|
+
...FS_SCOPE_REASONS,
|
|
1410
|
+
JUDGE_CLOUD_CONSENT_REASON
|
|
1411
|
+
]);
|
|
1118
1412
|
function shouldSkipBrokerApprovedOnce(brokerActive, reason) {
|
|
1119
1413
|
return brokerActive && FS_SCOPE_REASONS.has(reason);
|
|
1120
1414
|
}
|
|
1415
|
+
function shouldSkipBrokerApprovedRecord(brokerActive, approvalReason) {
|
|
1416
|
+
return brokerActive && approvalReason !== void 0 && FS_SCOPE_REASONS.has(approvalReason);
|
|
1417
|
+
}
|
|
1121
1418
|
|
|
1122
1419
|
// src/core/config-layers.ts
|
|
1123
|
-
import
|
|
1420
|
+
import path8 from "node:path";
|
|
1124
1421
|
|
|
1125
1422
|
// src/presets.ts
|
|
1126
1423
|
var CONFIG_PRESETS = {
|
|
@@ -1220,8 +1517,8 @@ function applyConfigPreset(preset, extra = {}) {
|
|
|
1220
1517
|
// src/core/config-layers.ts
|
|
1221
1518
|
function teamConfigPath(homedir = () => process.env.HOME ?? process.env.USERPROFILE ?? "") {
|
|
1222
1519
|
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
1223
|
-
const base = xdg ||
|
|
1224
|
-
return
|
|
1520
|
+
const base = xdg || path8.join(homedir(), ".config");
|
|
1521
|
+
return path8.join(base, "agent-belay", "team.config.json");
|
|
1225
1522
|
}
|
|
1226
1523
|
function applyProtectedLayer(config, builtin) {
|
|
1227
1524
|
const controlPlane = { ...config.controlPlane };
|
|
@@ -1255,6 +1552,10 @@ function resolveLayeredConfig(params) {
|
|
|
1255
1552
|
if (params.teamConfig) {
|
|
1256
1553
|
const teamFile = params.teamConfig;
|
|
1257
1554
|
const teamRaw = teamFile.preset ? applyConfigPreset(teamFile.preset, teamFile.config ?? {}) : teamFile.config ?? params.teamConfig;
|
|
1555
|
+
rejectTeamLayerJudgeSecrets(
|
|
1556
|
+
teamRaw.judge,
|
|
1557
|
+
"team"
|
|
1558
|
+
);
|
|
1258
1559
|
config = mergeConfigLayer(config, asV3Layer(teamRaw));
|
|
1259
1560
|
provenance.push({
|
|
1260
1561
|
path: params.teamConfigPath ?? teamConfigPath(),
|
|
@@ -1458,7 +1759,7 @@ function classifySubagent(payload, repoRoot, options = {}) {
|
|
|
1458
1759
|
}
|
|
1459
1760
|
|
|
1460
1761
|
// src/core/classify-tool.ts
|
|
1461
|
-
import
|
|
1762
|
+
import path16 from "node:path";
|
|
1462
1763
|
|
|
1463
1764
|
// src/core/glob.ts
|
|
1464
1765
|
function matchesSensitivePath(filePath, patterns) {
|
|
@@ -1503,7 +1804,7 @@ function matchesSensitivePath(filePath, patterns) {
|
|
|
1503
1804
|
return false;
|
|
1504
1805
|
}
|
|
1505
1806
|
|
|
1506
|
-
// src/core/
|
|
1807
|
+
// src/core/verdict/judge-audit.ts
|
|
1507
1808
|
function judgeTraceAuditFields(trace) {
|
|
1508
1809
|
if (!trace) {
|
|
1509
1810
|
return {};
|
|
@@ -1518,55 +1819,465 @@ function judgeTraceAuditFields(trace) {
|
|
|
1518
1819
|
};
|
|
1519
1820
|
}
|
|
1520
1821
|
|
|
1521
|
-
// src/
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1822
|
+
// src/adapters/layouts/codex.ts
|
|
1823
|
+
import path9 from "node:path";
|
|
1824
|
+
function runnerCommand2(platform, repoRoot, hookName, ...args) {
|
|
1825
|
+
const hooksDir = path9.join(path9.resolve(repoRoot), ".codex", "hooks");
|
|
1826
|
+
return buildRunnerInvocation(platform, hooksDir, repoRoot, hookName, ...args);
|
|
1827
|
+
}
|
|
1828
|
+
var codexLayout = {
|
|
1829
|
+
name: "codex",
|
|
1830
|
+
configPath(repoRoot) {
|
|
1831
|
+
return path9.join(repoRoot, ".codex", "belay.config.json");
|
|
1832
|
+
},
|
|
1833
|
+
// Codex reads lifecycle hooks from `.codex/config.toml` (project layer).
|
|
1834
|
+
hooksSettingsPath(repoRoot) {
|
|
1835
|
+
return path9.join(repoRoot, ".codex", "config.toml");
|
|
1836
|
+
},
|
|
1837
|
+
hooksDir(repoRoot) {
|
|
1838
|
+
return path9.join(repoRoot, ".codex", "hooks");
|
|
1839
|
+
},
|
|
1840
|
+
runtimeDir(repoRoot) {
|
|
1841
|
+
return path9.join(repoRoot, ".codex", "belay", "runtime");
|
|
1842
|
+
},
|
|
1843
|
+
repoLocalStateDir(repoRoot) {
|
|
1844
|
+
return path9.join(repoRoot, ".codex", "belay");
|
|
1845
|
+
},
|
|
1846
|
+
defaultAuditLogPath(_repoRoot) {
|
|
1847
|
+
return path9.join(".codex", "belay", "audit.ndjson");
|
|
1848
|
+
},
|
|
1849
|
+
repoRootMarkers: [".git", ".codex"],
|
|
1850
|
+
runnerCommand: runnerCommand2,
|
|
1851
|
+
defaultConfig(repoRoot) {
|
|
1852
|
+
return {
|
|
1853
|
+
...DEFAULT_CONFIG_V4,
|
|
1854
|
+
adapter: "codex",
|
|
1855
|
+
audit: {
|
|
1856
|
+
...DEFAULT_CONFIG_V4.audit,
|
|
1857
|
+
logPath: codexLayout.defaultAuditLogPath(repoRoot)
|
|
1858
|
+
}
|
|
1859
|
+
};
|
|
1532
1860
|
}
|
|
1533
|
-
|
|
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);
|
|
1534
1868
|
}
|
|
1535
|
-
|
|
1536
|
-
|
|
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];
|
|
1537
1911
|
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
return true;
|
|
1544
|
-
}
|
|
1912
|
+
|
|
1913
|
+
// src/config-io.ts
|
|
1914
|
+
function resolveAdapterName(config) {
|
|
1915
|
+
if (config.adapter === "claude") {
|
|
1916
|
+
return "claude";
|
|
1545
1917
|
}
|
|
1546
|
-
|
|
1918
|
+
if (config.adapter === "codex") {
|
|
1919
|
+
return "codex";
|
|
1920
|
+
}
|
|
1921
|
+
return "cursor";
|
|
1547
1922
|
}
|
|
1548
|
-
function
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
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);
|
|
1552
1978
|
return true;
|
|
1979
|
+
} catch {
|
|
1553
1980
|
}
|
|
1554
1981
|
}
|
|
1555
1982
|
return false;
|
|
1556
1983
|
}
|
|
1557
|
-
function
|
|
1558
|
-
const
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
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 };
|
|
1562
1988
|
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
if (segments.length > 1) {
|
|
1566
|
-
return token.replace(basename, "[REDACTED]");
|
|
1989
|
+
if (env.BELAY_JUDGE_DISABLE_CLI_TRANSPORT === "1") {
|
|
1990
|
+
return { http: true, cliTransport: null };
|
|
1567
1991
|
}
|
|
1568
|
-
|
|
1569
|
-
}
|
|
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
|
+
}
|
|
1570
2281
|
function scrubOutboundForJudge(text, options) {
|
|
1571
2282
|
try {
|
|
1572
2283
|
let scrubbed = scrubString(text, {
|
|
@@ -1587,37 +2298,57 @@ function scrubOutboundForJudge(text, options) {
|
|
|
1587
2298
|
}
|
|
1588
2299
|
}
|
|
1589
2300
|
|
|
1590
|
-
// src/core/
|
|
2301
|
+
// src/core/verdict/judge.ts
|
|
1591
2302
|
var SECRET_PATTERNS = [/\.env\b/i, /\.pem\b/i, /id_rsa\b/i, /credentials/i, /secrets?\b/i];
|
|
1592
2303
|
var DESTRUCTIVE_VERBS = /\b(rm|rmtree|unlink|delete|truncate|shred|destroy|drop)\b/i;
|
|
1593
2304
|
var GIT_PATTERNS = /\.git\b/i;
|
|
1594
|
-
var TIER1_PROMPT = `Answer ONLY with JSON: {"
|
|
1595
|
-
|
|
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).
|
|
1596
2312
|
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
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.
|
|
1600
2316
|
|
|
1601
2317
|
Command/code:
|
|
1602
2318
|
`;
|
|
1603
2319
|
function failClosedVerdict(reason) {
|
|
1604
2320
|
return {
|
|
1605
|
-
|
|
2321
|
+
local_recoverable: false,
|
|
1606
2322
|
destroys_outside_repo: true,
|
|
1607
2323
|
destroys_history_or_secrets: true,
|
|
2324
|
+
external_change: true,
|
|
1608
2325
|
reason
|
|
1609
2326
|
};
|
|
1610
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
|
+
}
|
|
1611
2340
|
function parseTier1Json(raw) {
|
|
1612
2341
|
try {
|
|
1613
2342
|
const parsed = JSON.parse(raw);
|
|
1614
|
-
|
|
2343
|
+
const localRecoverable = parseLocalRecoverable(parsed);
|
|
2344
|
+
if (localRecoverable === null) {
|
|
1615
2345
|
return null;
|
|
1616
2346
|
}
|
|
1617
2347
|
return {
|
|
1618
|
-
|
|
1619
|
-
destroys_outside_repo: parsed.destroys_outside_repo
|
|
1620
|
-
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,
|
|
1621
2352
|
reason: typeof parsed.reason === "string" ? parsed.reason : "tier1_llm"
|
|
1622
2353
|
};
|
|
1623
2354
|
} catch {
|
|
@@ -1631,7 +2362,7 @@ function prescanInterpreterCode(code) {
|
|
|
1631
2362
|
const hitsDestructive = DESTRUCTIVE_VERBS.test(normalized);
|
|
1632
2363
|
if ((hitsSecret || hitsGit) && hitsDestructive) {
|
|
1633
2364
|
return {
|
|
1634
|
-
|
|
2365
|
+
local_recoverable: true,
|
|
1635
2366
|
destroys_outside_repo: false,
|
|
1636
2367
|
destroys_history_or_secrets: true,
|
|
1637
2368
|
reason: "prescan_destructive_secret"
|
|
@@ -1639,11 +2370,49 @@ function prescanInterpreterCode(code) {
|
|
|
1639
2370
|
}
|
|
1640
2371
|
return null;
|
|
1641
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
|
+
}
|
|
1642
2411
|
function createDeterministicJudgeStub() {
|
|
1643
2412
|
return {
|
|
1644
2413
|
evaluate() {
|
|
1645
2414
|
return Promise.resolve({
|
|
1646
|
-
|
|
2415
|
+
local_recoverable: true,
|
|
1647
2416
|
destroys_outside_repo: false,
|
|
1648
2417
|
destroys_history_or_secrets: false,
|
|
1649
2418
|
reason: "deterministic_stub"
|
|
@@ -1775,8 +2544,7 @@ function createOpenAiCompatibleJudge(options) {
|
|
|
1775
2544
|
};
|
|
1776
2545
|
return failClosedVerdict("outbound_scrub_failed");
|
|
1777
2546
|
}
|
|
1778
|
-
const
|
|
1779
|
-
const resolvedKey = resolveJudgeApiKey2();
|
|
2547
|
+
const resolvedKey = options.resolveApiKey ? await options.resolveApiKey() : { key: options.apiKey ?? null, source: null };
|
|
1780
2548
|
const apiKey = options.apiKey ?? resolvedKey.key;
|
|
1781
2549
|
if (!apiKey) {
|
|
1782
2550
|
judge.lastTrace = {
|
|
@@ -1840,43 +2608,385 @@ function createOpenAiCompatibleJudge(options) {
|
|
|
1840
2608
|
return judge;
|
|
1841
2609
|
}
|
|
1842
2610
|
function tier1RequiresAsk(verdict2) {
|
|
1843
|
-
return verdict2.
|
|
2611
|
+
return !verdict2.local_recoverable || verdict2.destroys_history_or_secrets;
|
|
1844
2612
|
}
|
|
1845
2613
|
|
|
1846
|
-
// src/core/
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
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") {
|
|
1851
2650
|
return {
|
|
1852
|
-
|
|
1853
|
-
|
|
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
|
|
1854
2663
|
};
|
|
1855
2664
|
}
|
|
1856
|
-
|
|
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);
|
|
1857
2931
|
}
|
|
1858
2932
|
function createJudgeFromConfig(config, options = {}) {
|
|
2933
|
+
if (process.env.BELAY_DETERMINISTIC_JUDGE === "1") {
|
|
2934
|
+
return createDeterministicJudgeStub();
|
|
2935
|
+
}
|
|
1859
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
|
+
}
|
|
1860
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);
|
|
1861
2951
|
if (provider === "openai-compatible") {
|
|
1862
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
|
+
}
|
|
1863
2958
|
if (!endpoint) {
|
|
1864
2959
|
return createFailClosedJudge({
|
|
1865
2960
|
reason: "openai_compatible_endpoint_missing",
|
|
1866
2961
|
fallbackReason: "missing_endpoint",
|
|
1867
2962
|
modelRequested: judgeConfig.model,
|
|
1868
|
-
modelResolved:
|
|
2963
|
+
modelResolved: resolved
|
|
1869
2964
|
});
|
|
1870
2965
|
}
|
|
1871
|
-
|
|
1872
|
-
|
|
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);
|
|
1873
2976
|
return createOpenAiCompatibleJudge({
|
|
1874
2977
|
endpoint,
|
|
1875
2978
|
modelRequested: judgeConfig.model,
|
|
1876
2979
|
modelResolved: resolved,
|
|
1877
2980
|
timeoutMs: judgeConfig.timeoutMs,
|
|
1878
2981
|
sensitivePaths: config.classifier.sensitivePaths,
|
|
1879
|
-
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
|
+
})
|
|
1880
2990
|
});
|
|
1881
2991
|
}
|
|
1882
2992
|
if (provider === "ollama") {
|
|
@@ -1887,105 +2997,32 @@ function createJudgeFromConfig(config, options = {}) {
|
|
|
1887
2997
|
keepAlive: judgeConfig.keepAlive
|
|
1888
2998
|
});
|
|
1889
2999
|
}
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
if (token === "~" || token.startsWith("~/")) {
|
|
1900
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
1901
|
-
if (!home) {
|
|
1902
|
-
return token;
|
|
1903
|
-
}
|
|
1904
|
-
return token === "~" ? home : path8.join(home, token.slice(2));
|
|
1905
|
-
}
|
|
1906
|
-
return token;
|
|
1907
|
-
}
|
|
1908
|
-
function resolveTrustedPath(token, trustedCwd, trusted) {
|
|
1909
|
-
if (!token || token === "--" || token.startsWith("-")) {
|
|
1910
|
-
return null;
|
|
1911
|
-
}
|
|
1912
|
-
if (!trusted || !trustedCwd) {
|
|
1913
|
-
return null;
|
|
1914
|
-
}
|
|
1915
|
-
const expanded = expandHome(token);
|
|
1916
|
-
if (path8.isAbsolute(expanded)) {
|
|
1917
|
-
return canonicalPath(expanded);
|
|
1918
|
-
}
|
|
1919
|
-
return canonicalPath(path8.resolve(trustedCwd, expanded));
|
|
1920
|
-
}
|
|
1921
|
-
function locationForPath(resolvedPath, repoRoot) {
|
|
1922
|
-
if (!resolvedPath) {
|
|
1923
|
-
return "unknown";
|
|
1924
|
-
}
|
|
1925
|
-
if (pathWithinRoot(repoRoot, resolvedPath)) {
|
|
1926
|
-
return "repo_local";
|
|
1927
|
-
}
|
|
1928
|
-
return "repo_outside";
|
|
1929
|
-
}
|
|
1930
|
-
function isGitPath(resolvedPath, repoRoot) {
|
|
1931
|
-
const relative = relativeWithinRepo(repoRoot, resolvedPath);
|
|
1932
|
-
if (!relative) {
|
|
1933
|
-
return false;
|
|
1934
|
-
}
|
|
1935
|
-
const normalized = relative.replaceAll("\\", "/");
|
|
1936
|
-
return normalized === ".git" || normalized.startsWith(".git/");
|
|
1937
|
-
}
|
|
1938
|
-
function isHighStakesPath(resolvedPath, repoRoot, sensitivePaths, protectedRoots = []) {
|
|
1939
|
-
if (isGitPath(resolvedPath, repoRoot)) {
|
|
1940
|
-
return true;
|
|
1941
|
-
}
|
|
1942
|
-
const relative = relativeWithinRepo(repoRoot, resolvedPath);
|
|
1943
|
-
const checkPath = relative ?? resolvedPath;
|
|
1944
|
-
if (matchesSensitivePath(checkPath.replaceAll("\\", "/"), sensitivePaths)) {
|
|
1945
|
-
return true;
|
|
1946
|
-
}
|
|
1947
|
-
return protectedRoots.some((root) => pathWithinRoot(root, resolvedPath));
|
|
1948
|
-
}
|
|
1949
|
-
function analyzePathTargets(params) {
|
|
1950
|
-
const signals = [];
|
|
1951
|
-
if (!params.trustedCwd || !params.cwd) {
|
|
1952
|
-
return {
|
|
1953
|
-
location: "unknown",
|
|
1954
|
-
isHighStakes: false,
|
|
1955
|
-
signals: ["missing_trusted_cwd"]
|
|
1956
|
-
};
|
|
1957
|
-
}
|
|
1958
|
-
const locations = /* @__PURE__ */ new Set();
|
|
1959
|
-
let isHighStakes = false;
|
|
1960
|
-
for (const target of params.targets) {
|
|
1961
|
-
const resolved = resolveTrustedPath(target, params.cwd, params.trustedCwd) ?? resolveMutationTarget(target, params.cwd);
|
|
1962
|
-
const location2 = locationForPath(resolved, params.repoRoot);
|
|
1963
|
-
locations.add(location2);
|
|
1964
|
-
if (resolved && isHighStakesPath(
|
|
1965
|
-
resolved,
|
|
1966
|
-
params.repoRoot,
|
|
1967
|
-
params.sensitivePaths,
|
|
1968
|
-
params.protectedArtifactRoots
|
|
1969
|
-
)) {
|
|
1970
|
-
isHighStakes = true;
|
|
1971
|
-
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
|
+
});
|
|
1972
3009
|
}
|
|
3010
|
+
return createFailClosedJudge({
|
|
3011
|
+
reason: "anthropic_not_implemented",
|
|
3012
|
+
fallbackReason: "anthropic_runtime_unavailable",
|
|
3013
|
+
modelRequested: judgeConfig.model,
|
|
3014
|
+
modelResolved: resolved
|
|
3015
|
+
});
|
|
1973
3016
|
}
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
}
|
|
1980
|
-
location = "mixed";
|
|
1981
|
-
}
|
|
1982
|
-
return { location, isHighStakes, signals };
|
|
1983
|
-
}
|
|
1984
|
-
function cwdRelative(repoRoot, cwd) {
|
|
1985
|
-
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
|
+
});
|
|
1986
3023
|
}
|
|
1987
3024
|
|
|
1988
|
-
// src/core/
|
|
3025
|
+
// src/core/verdict/egress-classify.ts
|
|
1989
3026
|
var EGRESS_TOOL_HEADS = /* @__PURE__ */ new Set([
|
|
1990
3027
|
"aws",
|
|
1991
3028
|
"curl",
|
|
@@ -2083,6 +3120,9 @@ function classifyAws(tokens) {
|
|
|
2083
3120
|
if (/\bs3\s+rm\b/.test(joined)) {
|
|
2084
3121
|
return "destructive";
|
|
2085
3122
|
}
|
|
3123
|
+
if (/\bs3\s+mb\b/.test(joined)) {
|
|
3124
|
+
return "destructive";
|
|
3125
|
+
}
|
|
2086
3126
|
if (/\bs3\s+sync\b/.test(joined)) {
|
|
2087
3127
|
return "destructive";
|
|
2088
3128
|
}
|
|
@@ -2193,17 +3233,61 @@ function classifyNetlify(tokens) {
|
|
|
2193
3233
|
return "ambiguous";
|
|
2194
3234
|
}
|
|
2195
3235
|
|
|
2196
|
-
// src/core/
|
|
3236
|
+
// src/core/verdict/fingerprint.ts
|
|
2197
3237
|
function verdictFingerprint(cwdRelative2, commandRedacted) {
|
|
2198
3238
|
return hashValue(`v2:${cwdRelative2}:${commandRedacted}`);
|
|
2199
3239
|
}
|
|
2200
3240
|
|
|
2201
|
-
// src/core/
|
|
3241
|
+
// src/core/verdict/launcher-resolve.ts
|
|
2202
3242
|
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
|
|
2203
|
-
import
|
|
3243
|
+
import path14 from "node:path";
|
|
2204
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
|
+
]);
|
|
2205
3289
|
function readPackageJson(dir) {
|
|
2206
|
-
const packagePath =
|
|
3290
|
+
const packagePath = path14.join(dir, "package.json");
|
|
2207
3291
|
if (!existsSync4(packagePath)) {
|
|
2208
3292
|
return null;
|
|
2209
3293
|
}
|
|
@@ -2214,17 +3298,17 @@ function readPackageJson(dir) {
|
|
|
2214
3298
|
}
|
|
2215
3299
|
}
|
|
2216
3300
|
function findPackageJson(startDir, stopDir) {
|
|
2217
|
-
let current =
|
|
2218
|
-
const stop =
|
|
3301
|
+
let current = path14.resolve(startDir);
|
|
3302
|
+
const stop = path14.resolve(stopDir);
|
|
2219
3303
|
while (true) {
|
|
2220
|
-
const packagePath =
|
|
3304
|
+
const packagePath = path14.join(current, "package.json");
|
|
2221
3305
|
if (existsSync4(packagePath)) {
|
|
2222
3306
|
return packagePath;
|
|
2223
3307
|
}
|
|
2224
|
-
if (current === stop || current ===
|
|
3308
|
+
if (current === stop || current === path14.dirname(current)) {
|
|
2225
3309
|
return existsSync4(packagePath) ? packagePath : null;
|
|
2226
3310
|
}
|
|
2227
|
-
const parent =
|
|
3311
|
+
const parent = path14.dirname(current);
|
|
2228
3312
|
if (!parent.startsWith(stop) && parent !== current) {
|
|
2229
3313
|
}
|
|
2230
3314
|
if (parent === current) {
|
|
@@ -2256,6 +3340,12 @@ function npmScriptName(tokens) {
|
|
|
2256
3340
|
if (launcher[0] === "pnpm" && launcher[1] === "run" && launcher[2]) {
|
|
2257
3341
|
return launcher[2];
|
|
2258
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
|
+
}
|
|
2259
3349
|
if (launcher[0] === "npm" && launcher[1] && launcher[1] !== "run" && launcher[1] !== "install") {
|
|
2260
3350
|
return null;
|
|
2261
3351
|
}
|
|
@@ -2275,7 +3365,7 @@ function resolveNpmRecipe(cwd, repoRoot, scriptName, extraArgs) {
|
|
|
2275
3365
|
}
|
|
2276
3366
|
return { recipes: [], opaque: true, reason: "package_json_missing" };
|
|
2277
3367
|
}
|
|
2278
|
-
const pkg = readPackageJson(
|
|
3368
|
+
const pkg = readPackageJson(path14.dirname(packagePath));
|
|
2279
3369
|
const scripts = pkg?.scripts;
|
|
2280
3370
|
if (!scripts || typeof scripts !== "object") {
|
|
2281
3371
|
return { recipes: [], opaque: true, reason: "package_scripts_missing" };
|
|
@@ -2340,20 +3430,20 @@ function parseMakefileRecipes(makefilePath) {
|
|
|
2340
3430
|
function resolveMakeRecipe(cwd, repoRoot, target) {
|
|
2341
3431
|
const candidates = ["Makefile", "makefile", "GNUmakefile"];
|
|
2342
3432
|
let makefilePath = null;
|
|
2343
|
-
let searchDir =
|
|
2344
|
-
const stop =
|
|
3433
|
+
let searchDir = path14.resolve(cwd);
|
|
3434
|
+
const stop = path14.resolve(repoRoot);
|
|
2345
3435
|
while (true) {
|
|
2346
3436
|
for (const name of candidates) {
|
|
2347
|
-
const candidate =
|
|
3437
|
+
const candidate = path14.join(searchDir, name);
|
|
2348
3438
|
if (existsSync4(candidate)) {
|
|
2349
3439
|
makefilePath = candidate;
|
|
2350
3440
|
break;
|
|
2351
3441
|
}
|
|
2352
3442
|
}
|
|
2353
|
-
if (makefilePath || searchDir === stop || searchDir ===
|
|
3443
|
+
if (makefilePath || searchDir === stop || searchDir === path14.dirname(searchDir)) {
|
|
2354
3444
|
break;
|
|
2355
3445
|
}
|
|
2356
|
-
searchDir =
|
|
3446
|
+
searchDir = path14.dirname(searchDir);
|
|
2357
3447
|
}
|
|
2358
3448
|
if (!makefilePath) {
|
|
2359
3449
|
return { recipes: [], opaque: true, reason: "unknown_local_effect" };
|
|
@@ -2377,11 +3467,31 @@ function resolveLauncherRecipe(params) {
|
|
|
2377
3467
|
const tokens = params.tokens;
|
|
2378
3468
|
const scriptName = npmScriptName(tokens);
|
|
2379
3469
|
if (scriptName) {
|
|
2380
|
-
|
|
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;
|
|
2381
3484
|
}
|
|
2382
3485
|
if (tokens[0] === "make" && tokens[1] && !tokens[1].startsWith("-")) {
|
|
2383
3486
|
return resolveMakeRecipe(params.cwd, params.repoRoot, tokens[1]);
|
|
2384
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
|
+
}
|
|
2385
3495
|
return null;
|
|
2386
3496
|
}
|
|
2387
3497
|
function isRoutineLauncher(tokens) {
|
|
@@ -2397,7 +3507,7 @@ function matchesCustomCommand(normalizedCommand, key, pattern) {
|
|
|
2397
3507
|
return normalizedCommand === trimmed || key === trimmed;
|
|
2398
3508
|
}
|
|
2399
3509
|
|
|
2400
|
-
// src/core/
|
|
3510
|
+
// src/core/verdict/overrides.ts
|
|
2401
3511
|
function matchesCustomPatterns(command, segment, patterns) {
|
|
2402
3512
|
if (!patterns || patterns.length === 0) {
|
|
2403
3513
|
return false;
|
|
@@ -2436,8 +3546,8 @@ function askFromCustomExternal(opacity) {
|
|
|
2436
3546
|
};
|
|
2437
3547
|
}
|
|
2438
3548
|
|
|
2439
|
-
// src/core/
|
|
2440
|
-
import
|
|
3549
|
+
// src/core/verdict/parser.ts
|
|
3550
|
+
import path15 from "node:path";
|
|
2441
3551
|
|
|
2442
3552
|
// src/core/shell-substitution.ts
|
|
2443
3553
|
function findCommandSubstitutions(command) {
|
|
@@ -2660,7 +3770,7 @@ function hasUnbalancedDollarParen(command) {
|
|
|
2660
3770
|
return depth > 0;
|
|
2661
3771
|
}
|
|
2662
3772
|
|
|
2663
|
-
// src/core/
|
|
3773
|
+
// src/core/verdict/parser.ts
|
|
2664
3774
|
var ENV_PREFIX_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=(?:'[^']*'|"[^"]*"|\S+)$/;
|
|
2665
3775
|
var TRANSPARENT_WRAPPERS = /* @__PURE__ */ new Set([
|
|
2666
3776
|
"sudo",
|
|
@@ -2687,7 +3797,7 @@ var INTERPRETER_SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
2687
3797
|
".sh"
|
|
2688
3798
|
]);
|
|
2689
3799
|
function normalizeHead(token) {
|
|
2690
|
-
const base =
|
|
3800
|
+
const base = path15.basename(token);
|
|
2691
3801
|
if (base && base !== "." && base !== "..") {
|
|
2692
3802
|
return base;
|
|
2693
3803
|
}
|
|
@@ -2792,7 +3902,7 @@ function isBareInterpreter(tokens) {
|
|
|
2792
3902
|
return false;
|
|
2793
3903
|
}
|
|
2794
3904
|
const scriptArg = args.find((token) => !token.startsWith("-"));
|
|
2795
|
-
if (scriptArg && INTERPRETER_SCRIPT_EXTENSIONS.has(
|
|
3905
|
+
if (scriptArg && INTERPRETER_SCRIPT_EXTENSIONS.has(path15.extname(scriptArg))) {
|
|
2796
3906
|
return false;
|
|
2797
3907
|
}
|
|
2798
3908
|
if (scriptArg) {
|
|
@@ -2860,7 +3970,7 @@ function redactCommand(command) {
|
|
|
2860
3970
|
return command.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]").replace(/sk-[A-Za-z0-9]{8,}/g, "sk-[REDACTED]").trim();
|
|
2861
3971
|
}
|
|
2862
3972
|
|
|
2863
|
-
// src/core/
|
|
3973
|
+
// src/core/verdict/verdict.ts
|
|
2864
3974
|
var DEFAULT_MAX_DEPTH = 8;
|
|
2865
3975
|
var TIER0_EXTERNAL_KEYS = /* @__PURE__ */ new Set([
|
|
2866
3976
|
"git push",
|
|
@@ -2943,6 +4053,7 @@ var LOCAL_ROUTINE_HEADS = /* @__PURE__ */ new Set([
|
|
|
2943
4053
|
"make",
|
|
2944
4054
|
"cmake"
|
|
2945
4055
|
]);
|
|
4056
|
+
var BELAY_SELF_COMMANDS = /* @__PURE__ */ new Set(["approve", "revoke"]);
|
|
2946
4057
|
var FIND_DANGEROUS_FLAGS = /* @__PURE__ */ new Set(["-delete", "-exec", "-execdir", "-ok", "-okdir"]);
|
|
2947
4058
|
function isFindDangerous(tokens) {
|
|
2948
4059
|
return tokens.some(
|
|
@@ -3090,8 +4201,36 @@ function tier0ExternalMatch(key, head, tokens) {
|
|
|
3090
4201
|
}
|
|
3091
4202
|
return false;
|
|
3092
4203
|
}
|
|
3093
|
-
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) {
|
|
3094
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() ?? "";
|
|
3095
4234
|
if (head !== "rm") {
|
|
3096
4235
|
return null;
|
|
3097
4236
|
}
|
|
@@ -3114,34 +4253,44 @@ function tier0HighStakesRm(tokens, context) {
|
|
|
3114
4253
|
signals: ["missing_trusted_cwd", ...analysis.signals]
|
|
3115
4254
|
});
|
|
3116
4255
|
}
|
|
3117
|
-
if (analysis.isHighStakes) {
|
|
3118
|
-
return askVerdict({
|
|
3119
|
-
location: analysis.location,
|
|
3120
|
-
opacity: "transparent",
|
|
3121
|
-
effect: "local_mutation",
|
|
3122
|
-
confidence: "deterministic",
|
|
3123
|
-
reason: "high_stakes_path",
|
|
3124
|
-
signals: ["high_stakes_path", ...analysis.signals]
|
|
3125
|
-
});
|
|
3126
|
-
}
|
|
3127
4256
|
for (const target of targets) {
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
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;
|
|
3143
4282
|
}
|
|
3144
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
|
+
}
|
|
3145
4294
|
}
|
|
3146
4295
|
return null;
|
|
3147
4296
|
}
|
|
@@ -3321,6 +4470,16 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3321
4470
|
if (rmVerdict) {
|
|
3322
4471
|
return rmVerdict;
|
|
3323
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
|
+
}
|
|
3324
4483
|
let effect = "unknown";
|
|
3325
4484
|
if (READ_ONLY_KEYS.has(segment.key) || READ_ONLY_KEYS.has(segment.head)) {
|
|
3326
4485
|
effect = "read_only";
|
|
@@ -3330,6 +4489,9 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3330
4489
|
effect = "local_mutation";
|
|
3331
4490
|
}
|
|
3332
4491
|
const pathArgs = extractPathArgs(peeled);
|
|
4492
|
+
if (extractRedirectTargets(peeled).length > 0 && effect === "read_only") {
|
|
4493
|
+
effect = "local_mutation";
|
|
4494
|
+
}
|
|
3333
4495
|
const pathAnalysis = analyzePathTargets({
|
|
3334
4496
|
targets: pathArgs,
|
|
3335
4497
|
cwd: context.cwd,
|
|
@@ -3360,15 +4522,46 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3360
4522
|
});
|
|
3361
4523
|
}
|
|
3362
4524
|
}
|
|
3363
|
-
if (
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
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
|
+
}
|
|
3372
4565
|
}
|
|
3373
4566
|
if (segment.head === "find" && isFindDangerous(peeled)) {
|
|
3374
4567
|
return askVerdict({
|
|
@@ -3380,15 +4573,14 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3380
4573
|
signals: ["find_dangerous_action"]
|
|
3381
4574
|
});
|
|
3382
4575
|
}
|
|
3383
|
-
if (pathAnalysis.location === "repo_outside" || pathAnalysis.location === "mixed") {
|
|
3384
|
-
|
|
3385
|
-
return askVerdict({
|
|
4576
|
+
if (effect === "read_only" && (pathAnalysis.location === "repo_outside" || pathAnalysis.location === "mixed")) {
|
|
4577
|
+
return allowVerdict({
|
|
3386
4578
|
location: pathAnalysis.location,
|
|
3387
|
-
opacity
|
|
3388
|
-
effect:
|
|
4579
|
+
opacity,
|
|
4580
|
+
effect: "read_only",
|
|
3389
4581
|
confidence: "deterministic",
|
|
3390
|
-
reason: "
|
|
3391
|
-
signals: ["
|
|
4582
|
+
reason: "read_only",
|
|
4583
|
+
signals: ["read_only", ...pathAnalysis.signals]
|
|
3392
4584
|
});
|
|
3393
4585
|
}
|
|
3394
4586
|
if (pathAnalysis.location === "unknown" && pathArgs.length > 0 && LOCAL_MUTATION_KEYS.has(segment.head)) {
|
|
@@ -3401,7 +4593,25 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3401
4593
|
signals: ["unknown_location_mutation"]
|
|
3402
4594
|
});
|
|
3403
4595
|
}
|
|
3404
|
-
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";
|
|
3405
4615
|
let tier1Trace;
|
|
3406
4616
|
if (needsTier1) {
|
|
3407
4617
|
const tier1Text = recursiveScript ?? command;
|
|
@@ -3413,9 +4623,9 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3413
4623
|
tier1Trace = context.judge.lastTrace;
|
|
3414
4624
|
if (tier1RequiresAsk(tier1)) {
|
|
3415
4625
|
return askVerdict({
|
|
3416
|
-
location: pathAnalysis.location === "unknown" ? "unknown" : "repo_local",
|
|
4626
|
+
location: pathAnalysis.location === "unknown" ? "unknown" : pathAnalysis.location === "repo_outside" || pathAnalysis.location === "mixed" ? pathAnalysis.location : "repo_local",
|
|
3417
4627
|
opacity,
|
|
3418
|
-
effect: tier1.
|
|
4628
|
+
effect: !tier1.local_recoverable ? "remote_mutation" : effect,
|
|
3419
4629
|
confidence: "llm",
|
|
3420
4630
|
reason: "tier1_catastrophic",
|
|
3421
4631
|
signals: ["tier1_catastrophic", tier1.reason],
|
|
@@ -3423,6 +4633,19 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3423
4633
|
});
|
|
3424
4634
|
}
|
|
3425
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
|
+
}
|
|
3426
4649
|
if (pathAnalysis.location === "repo_local" && (effect === "read_only" || effect === "local_mutation") && opacity !== "opaque") {
|
|
3427
4650
|
return withJudgeTrace(
|
|
3428
4651
|
allowVerdict({
|
|
@@ -3541,7 +4764,13 @@ async function verdict(command, context) {
|
|
|
3541
4764
|
);
|
|
3542
4765
|
}
|
|
3543
4766
|
|
|
3544
|
-
// 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
|
+
}
|
|
3545
4774
|
function buildVerdictContext(params) {
|
|
3546
4775
|
const protectedArtifactRoots2 = [
|
|
3547
4776
|
...params.options?.protectedArtifactRoots ?? [],
|
|
@@ -3550,7 +4779,7 @@ function buildVerdictContext(params) {
|
|
|
3550
4779
|
return {
|
|
3551
4780
|
cwd: params.cwd,
|
|
3552
4781
|
repoRoot: params.repoRoot,
|
|
3553
|
-
trustedCwd: params.
|
|
4782
|
+
trustedCwd: resolveClassifierTrustedCwd(params.cwd, params.options, params.trustedCwd),
|
|
3554
4783
|
sensitivePaths: params.options?.sensitivePaths ?? params.config.classifier.sensitivePaths,
|
|
3555
4784
|
protectedArtifactRoots: protectedArtifactRoots2.length > 0 ? [...new Set(protectedArtifactRoots2)] : void 0,
|
|
3556
4785
|
customAllowCommands: params.options?.customAllowCommands ?? params.config.overrides.allow,
|
|
@@ -3588,6 +4817,12 @@ function mapLegacyReason(result) {
|
|
|
3588
4817
|
if (result.reason === "repo_local_mutation") {
|
|
3589
4818
|
return "local_mutation";
|
|
3590
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
|
+
}
|
|
3591
4826
|
return result.reason;
|
|
3592
4827
|
}
|
|
3593
4828
|
function verdictToClassifyResult(result) {
|
|
@@ -3608,13 +4843,13 @@ function verdictToClassifyResult(result) {
|
|
|
3608
4843
|
assessment,
|
|
3609
4844
|
normalizedCommand: result.commandRedacted,
|
|
3610
4845
|
summary: result.commandRedacted,
|
|
3611
|
-
|
|
4846
|
+
axes: {
|
|
3612
4847
|
location: result.location,
|
|
3613
4848
|
opacity: result.opacity,
|
|
3614
4849
|
effect: result.effect,
|
|
3615
4850
|
confidence: result.confidence,
|
|
3616
4851
|
would: result.permission,
|
|
3617
|
-
by: "
|
|
4852
|
+
by: "verdict",
|
|
3618
4853
|
commandRedacted: result.commandRedacted,
|
|
3619
4854
|
commandFingerprint: result.fingerprint,
|
|
3620
4855
|
signals: result.signals,
|
|
@@ -3676,7 +4911,7 @@ function extractPatch(payload) {
|
|
|
3676
4911
|
}
|
|
3677
4912
|
return null;
|
|
3678
4913
|
}
|
|
3679
|
-
function
|
|
4914
|
+
function applyPatchTargets2(patch) {
|
|
3680
4915
|
const targets = [];
|
|
3681
4916
|
for (const line of patch.split("\n")) {
|
|
3682
4917
|
const match = line.match(/^\*\*\* (Add|Delete|Update) File: (.+)$/);
|
|
@@ -3694,6 +4929,70 @@ function applyPatchTargets(patch) {
|
|
|
3694
4929
|
function normalizedToolName(toolName) {
|
|
3695
4930
|
return toolName.trim().toLowerCase();
|
|
3696
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
|
+
}
|
|
3697
4996
|
async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
|
|
3698
4997
|
const toolName = String(payload.tool_name ?? "");
|
|
3699
4998
|
const toolKind = normalizedToolName(toolName);
|
|
@@ -3789,7 +5088,7 @@ async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
|
|
|
3789
5088
|
};
|
|
3790
5089
|
}
|
|
3791
5090
|
const signals = [];
|
|
3792
|
-
const resolvedPath =
|
|
5091
|
+
const resolvedPath = path16.isAbsolute(filePath) ? filePath : path16.resolve(cwd, filePath);
|
|
3793
5092
|
const hitsProtectedRoot = protectedRoots.some((root) => pathWithinRoot(root, resolvedPath));
|
|
3794
5093
|
if (hitsProtectedRoot) {
|
|
3795
5094
|
signals.push("control_plane_path");
|
|
@@ -3810,35 +5109,35 @@ async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
|
|
|
3810
5109
|
const relativePath = relativeWithinRepo(repoRoot, resolvedPath);
|
|
3811
5110
|
if (relativePath === null) {
|
|
3812
5111
|
signals.push("outside_repo_path");
|
|
3813
|
-
return {
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
};
|
|
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
|
+
});
|
|
3826
5125
|
}
|
|
3827
5126
|
if (matchesSensitivePath(relativePath, sensitivePaths)) {
|
|
3828
5127
|
signals.push("sensitive_path");
|
|
3829
|
-
return {
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
};
|
|
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
|
+
});
|
|
3842
5141
|
}
|
|
3843
5142
|
if (FILE_DELETE_TOOL_NAMES.has(toolKind)) {
|
|
3844
5143
|
signals.push("file_delete");
|
|
@@ -3873,7 +5172,7 @@ async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
|
|
|
3873
5172
|
}
|
|
3874
5173
|
if (APPLY_PATCH_TOOL_NAMES.has(toolKind)) {
|
|
3875
5174
|
const patch = extractPatch(payload);
|
|
3876
|
-
const targets = patch ?
|
|
5175
|
+
const targets = patch ? applyPatchTargets2(patch) : [];
|
|
3877
5176
|
if (targets.length === 0) {
|
|
3878
5177
|
if (options.unknownLocalEffect === "deny") {
|
|
3879
5178
|
return {
|
|
@@ -4063,10 +5362,56 @@ function normalizeGatedAction(params) {
|
|
|
4063
5362
|
agentAssessment
|
|
4064
5363
|
};
|
|
4065
5364
|
}
|
|
4066
|
-
function
|
|
4067
|
-
if (options.brokerFsScope
|
|
4068
|
-
|
|
4069
|
-
|
|
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") {
|
|
4070
5415
|
return {
|
|
4071
5416
|
...result,
|
|
4072
5417
|
verdict: "allow_flagged",
|
|
@@ -4084,6 +5429,38 @@ function applyShellPeripheralPolicy(command, action, result, options) {
|
|
|
4084
5429
|
}
|
|
4085
5430
|
return result;
|
|
4086
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
|
+
}
|
|
4087
5464
|
async function classifyGatedAction(action, config, extraOptions = {}) {
|
|
4088
5465
|
const options = { ...classifierOptionsFromConfig(config), ...extraOptions };
|
|
4089
5466
|
if (action.kind === "shell") {
|
|
@@ -4091,26 +5468,47 @@ async function classifyGatedAction(action, config, extraOptions = {}) {
|
|
|
4091
5468
|
if (!command) {
|
|
4092
5469
|
throw new GateNormalizationError("Shell gated action requires a command.");
|
|
4093
5470
|
}
|
|
4094
|
-
let
|
|
4095
|
-
|
|
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);
|
|
4096
5474
|
if (!action.agentAssessment) {
|
|
4097
|
-
return
|
|
5475
|
+
return result2;
|
|
4098
5476
|
}
|
|
4099
|
-
const
|
|
4100
|
-
if (!
|
|
4101
|
-
return { ...
|
|
5477
|
+
const merged2 = mergeAgentAssessment(result2.assessment, action.agentAssessment);
|
|
5478
|
+
if (!merged2.mismatch) {
|
|
5479
|
+
return { ...result2, assessment: merged2.assessment };
|
|
4102
5480
|
}
|
|
4103
5481
|
return {
|
|
4104
|
-
...
|
|
5482
|
+
...result2,
|
|
4105
5483
|
verdict: "deny_pending_approval",
|
|
4106
5484
|
reason: "agent_assessment_mismatch",
|
|
4107
|
-
assessment:
|
|
5485
|
+
assessment: merged2.assessment
|
|
4108
5486
|
};
|
|
4109
5487
|
}
|
|
4110
5488
|
if (action.kind === "subagent") {
|
|
4111
5489
|
return classifySubagent(action.payload ?? {}, action.repoRoot, options);
|
|
4112
5490
|
}
|
|
4113
|
-
|
|
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
|
+
};
|
|
4114
5512
|
}
|
|
4115
5513
|
async function classifyGatedActionAsync(action, config, extraOptions = {}) {
|
|
4116
5514
|
return classifyGatedAction(action, config, extraOptions);
|
|
@@ -4136,9 +5534,9 @@ function gateEnabledForAction(config, action) {
|
|
|
4136
5534
|
}
|
|
4137
5535
|
|
|
4138
5536
|
// src/core/transactional/diff-evaluator.ts
|
|
4139
|
-
import
|
|
5537
|
+
import path17 from "node:path";
|
|
4140
5538
|
function categorizeChange(change, ctx) {
|
|
4141
|
-
const absolutePath = canonicalPath(
|
|
5539
|
+
const absolutePath = canonicalPath(path17.join(ctx.repoRoot, change.relativePath));
|
|
4142
5540
|
if (!pathWithinRoot(ctx.repoRoot, absolutePath)) {
|
|
4143
5541
|
return "repo_outside";
|
|
4144
5542
|
}
|
|
@@ -4160,15 +5558,6 @@ function observedAssessment(evaluation) {
|
|
|
4160
5558
|
if (evaluation.deletedCount > 0) {
|
|
4161
5559
|
signals.push("observed_deletions");
|
|
4162
5560
|
}
|
|
4163
|
-
if (evaluation.categories.includes("repo_outside") || evaluation.categories.includes("control_plane") || evaluation.categories.includes("sensitive_path")) {
|
|
4164
|
-
return {
|
|
4165
|
-
reversibility: "irreversible",
|
|
4166
|
-
external: evaluation.categories.includes("repo_outside"),
|
|
4167
|
-
blastRadius: evaluation.categories.includes("control_plane") ? "agent-belay control plane" : evaluation.categories.includes("repo_outside") ? "outside the repository" : "sensitive path",
|
|
4168
|
-
confidence: 1,
|
|
4169
|
-
signals
|
|
4170
|
-
};
|
|
4171
|
-
}
|
|
4172
5561
|
if (evaluation.categories.includes("large_deletion")) {
|
|
4173
5562
|
return {
|
|
4174
5563
|
reversibility: "irreversible",
|
|
@@ -4196,7 +5585,7 @@ function evaluateTransactionalDiff(changes, ctx) {
|
|
|
4196
5585
|
categories.add("large_deletion");
|
|
4197
5586
|
}
|
|
4198
5587
|
const categoryList = [...categories];
|
|
4199
|
-
const dangerous = categoryList.includes("
|
|
5588
|
+
const dangerous = categoryList.includes("large_deletion");
|
|
4200
5589
|
const base = {
|
|
4201
5590
|
categories: categoryList,
|
|
4202
5591
|
changes,
|
|
@@ -4267,15 +5656,15 @@ var TRANSACTIONAL_APPROVAL_BYPASS_REASONS = /* @__PURE__ */ new Set([
|
|
|
4267
5656
|
]);
|
|
4268
5657
|
|
|
4269
5658
|
// src/core/transactional/git-worktree.ts
|
|
4270
|
-
import { spawn } from "node:child_process";
|
|
5659
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
4271
5660
|
import { randomUUID } from "node:crypto";
|
|
4272
5661
|
import { existsSync as existsSync5 } from "node:fs";
|
|
4273
|
-
import { copyFile, mkdir as
|
|
5662
|
+
import { copyFile, mkdir as mkdir3, mkdtemp, rm } from "node:fs/promises";
|
|
4274
5663
|
import os from "node:os";
|
|
4275
|
-
import
|
|
5664
|
+
import path18 from "node:path";
|
|
4276
5665
|
function execGit(repoRoot, args) {
|
|
4277
5666
|
return new Promise((resolve, reject) => {
|
|
4278
|
-
const child =
|
|
5667
|
+
const child = spawn2("git", ["-C", repoRoot, ...args], {
|
|
4279
5668
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4280
5669
|
});
|
|
4281
5670
|
const stdout = [];
|
|
@@ -4313,8 +5702,8 @@ async function isDirtyWorktree(repoRoot) {
|
|
|
4313
5702
|
}
|
|
4314
5703
|
}
|
|
4315
5704
|
async function createGitWorktreeSnapshot(repoRoot, stateDir) {
|
|
4316
|
-
const worktreePath =
|
|
4317
|
-
await
|
|
5705
|
+
const worktreePath = path18.join(stateDir, `tx-${randomUUID().replaceAll("-", "")}`);
|
|
5706
|
+
await mkdir3(stateDir, { recursive: true });
|
|
4318
5707
|
await execGit(repoRoot, ["worktree", "add", "--detach", worktreePath, "HEAD"]);
|
|
4319
5708
|
return {
|
|
4320
5709
|
worktreePath,
|
|
@@ -4333,18 +5722,18 @@ async function createGitWorktreeSnapshot(repoRoot, stateDir) {
|
|
|
4333
5722
|
}
|
|
4334
5723
|
function resolveWorktreeCwd(repoRoot, worktreePath, cwd) {
|
|
4335
5724
|
const resolvedCwd = canonicalPath(cwd);
|
|
4336
|
-
const relative =
|
|
4337
|
-
if (relative.startsWith("..") ||
|
|
5725
|
+
const relative = path18.relative(canonicalPath(repoRoot), resolvedCwd);
|
|
5726
|
+
if (relative.startsWith("..") || path18.isAbsolute(relative)) {
|
|
4338
5727
|
return worktreePath;
|
|
4339
5728
|
}
|
|
4340
5729
|
if (relative === "") {
|
|
4341
5730
|
return worktreePath;
|
|
4342
5731
|
}
|
|
4343
|
-
return
|
|
5732
|
+
return path18.join(worktreePath, relative);
|
|
4344
5733
|
}
|
|
4345
5734
|
function runShellCommand(command, cwd, timeoutMs) {
|
|
4346
5735
|
return new Promise((resolve) => {
|
|
4347
|
-
const child =
|
|
5736
|
+
const child = spawn2(command, {
|
|
4348
5737
|
cwd,
|
|
4349
5738
|
shell: true,
|
|
4350
5739
|
stdio: "ignore",
|
|
@@ -4410,7 +5799,7 @@ async function rollbackAppliedChanges(actions) {
|
|
|
4410
5799
|
for (const action of [...actions].reverse()) {
|
|
4411
5800
|
try {
|
|
4412
5801
|
if (action.type === "restore") {
|
|
4413
|
-
await
|
|
5802
|
+
await mkdir3(path18.dirname(action.target), { recursive: true });
|
|
4414
5803
|
await copyFile(action.backupPath, action.target);
|
|
4415
5804
|
} else {
|
|
4416
5805
|
await rm(action.target, { force: true });
|
|
@@ -4420,14 +5809,14 @@ async function rollbackAppliedChanges(actions) {
|
|
|
4420
5809
|
}
|
|
4421
5810
|
}
|
|
4422
5811
|
async function applyWorktreeChanges(worktreePath, repoRoot, changes) {
|
|
4423
|
-
const backupRoot = await mkdtemp(
|
|
5812
|
+
const backupRoot = await mkdtemp(path18.join(os.tmpdir(), "belay-tx-rollback-"));
|
|
4424
5813
|
const rollbackActions = [];
|
|
4425
5814
|
try {
|
|
4426
5815
|
for (const change of changes) {
|
|
4427
|
-
const target =
|
|
5816
|
+
const target = path18.join(repoRoot, change.relativePath);
|
|
4428
5817
|
if (existsSync5(target)) {
|
|
4429
|
-
const backupPath =
|
|
4430
|
-
await
|
|
5818
|
+
const backupPath = path18.join(backupRoot, change.relativePath);
|
|
5819
|
+
await mkdir3(path18.dirname(backupPath), { recursive: true });
|
|
4431
5820
|
await copyFile(target, backupPath);
|
|
4432
5821
|
rollbackActions.push({ type: "restore", target, backupPath });
|
|
4433
5822
|
} else if (change.kind !== "deleted") {
|
|
@@ -4437,8 +5826,8 @@ async function applyWorktreeChanges(worktreePath, repoRoot, changes) {
|
|
|
4437
5826
|
await rm(target, { force: true });
|
|
4438
5827
|
continue;
|
|
4439
5828
|
}
|
|
4440
|
-
const source =
|
|
4441
|
-
await
|
|
5829
|
+
const source = path18.join(worktreePath, change.relativePath);
|
|
5830
|
+
await mkdir3(path18.dirname(target), { recursive: true });
|
|
4442
5831
|
await copyFile(source, target);
|
|
4443
5832
|
}
|
|
4444
5833
|
} catch (error) {
|
|
@@ -4600,7 +5989,7 @@ async function notifyDeny(config, event) {
|
|
|
4600
5989
|
}
|
|
4601
5990
|
|
|
4602
5991
|
// src/adapters/layouts/protected-paths.ts
|
|
4603
|
-
import
|
|
5992
|
+
import path19 from "node:path";
|
|
4604
5993
|
function protectedArtifactRoots(layout, repoRoot, controlPlaneDir) {
|
|
4605
5994
|
const roots = [
|
|
4606
5995
|
layout.configPath(repoRoot),
|
|
@@ -4612,7 +6001,7 @@ function protectedArtifactRoots(layout, repoRoot, controlPlaneDir) {
|
|
|
4612
6001
|
if (controlPlaneDir) {
|
|
4613
6002
|
roots.push(controlPlaneDir);
|
|
4614
6003
|
}
|
|
4615
|
-
return roots.map((entry) =>
|
|
6004
|
+
return roots.map((entry) => path19.resolve(entry));
|
|
4616
6005
|
}
|
|
4617
6006
|
|
|
4618
6007
|
// src/adapters/shared/gate-runtime.ts
|
|
@@ -4622,7 +6011,7 @@ var EMPTY_APPROVALS = {
|
|
|
4622
6011
|
};
|
|
4623
6012
|
async function loadJsonFile(filePath, fallback) {
|
|
4624
6013
|
try {
|
|
4625
|
-
const raw = await
|
|
6014
|
+
const raw = await readFile3(filePath, "utf8");
|
|
4626
6015
|
return JSON.parse(raw);
|
|
4627
6016
|
} catch {
|
|
4628
6017
|
return fallback;
|
|
@@ -4634,14 +6023,14 @@ function createDefaultGateRuntimeDeps() {
|
|
|
4634
6023
|
return loadJsonFile(configPath, {});
|
|
4635
6024
|
},
|
|
4636
6025
|
async appendAudit(ctx, event) {
|
|
4637
|
-
const auditPath =
|
|
4638
|
-
await
|
|
6026
|
+
const auditPath = path20.join(ctx.repoRoot, ctx.config.audit.logPath);
|
|
6027
|
+
await mkdir4(path20.dirname(auditPath), { recursive: true });
|
|
4639
6028
|
const record = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), ...event };
|
|
4640
6029
|
if (!ctx.config.audit.includeAssessment) {
|
|
4641
6030
|
delete record.assessment;
|
|
4642
6031
|
}
|
|
4643
6032
|
const scrubbed = scrubValue(record, scrubOptionsFromConfig(ctx.config));
|
|
4644
|
-
await
|
|
6033
|
+
await writeFile3(auditPath, `${JSON.stringify(scrubbed)}
|
|
4645
6034
|
`, {
|
|
4646
6035
|
encoding: "utf8",
|
|
4647
6036
|
flag: "a"
|
|
@@ -4660,8 +6049,8 @@ function createDefaultGateRuntimeDeps() {
|
|
|
4660
6049
|
};
|
|
4661
6050
|
},
|
|
4662
6051
|
async writeApprovals(filePath, state) {
|
|
4663
|
-
await
|
|
4664
|
-
await
|
|
6052
|
+
await mkdir4(path20.dirname(filePath), { recursive: true });
|
|
6053
|
+
await writeFile3(filePath, `${JSON.stringify(compactApprovals(state), null, 2)}
|
|
4665
6054
|
`, "utf8");
|
|
4666
6055
|
}
|
|
4667
6056
|
};
|
|
@@ -4671,7 +6060,7 @@ async function resolveGateConfig(ctx, deps) {
|
|
|
4671
6060
|
let teamConfig = null;
|
|
4672
6061
|
const teamPath = teamConfigPath();
|
|
4673
6062
|
if (existsSync6(teamPath)) {
|
|
4674
|
-
teamConfig = JSON.parse(await
|
|
6063
|
+
teamConfig = JSON.parse(await readFile3(teamPath, "utf8"));
|
|
4675
6064
|
}
|
|
4676
6065
|
return resolveLayeredConfig({
|
|
4677
6066
|
repoConfig: loaded,
|
|
@@ -4743,7 +6132,15 @@ async function consumeApprovedApproval(ctx, deps, kind, fingerprint) {
|
|
|
4743
6132
|
await deps.writeApprovals(approved.filePath, approved.state);
|
|
4744
6133
|
return null;
|
|
4745
6134
|
}
|
|
4746
|
-
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
|
+
};
|
|
4747
6144
|
await deps.writeApprovals(approved.filePath, approved.state);
|
|
4748
6145
|
return approval;
|
|
4749
6146
|
}
|
|
@@ -4808,7 +6205,7 @@ async function evaluateGatedAction(ctx, deps, params) {
|
|
|
4808
6205
|
command: params.command,
|
|
4809
6206
|
cwd: params.cwd,
|
|
4810
6207
|
repoRoot: ctx.repoRoot,
|
|
4811
|
-
stateDir:
|
|
6208
|
+
stateDir: path20.join(ctx.layout.repoLocalStateDir(ctx.repoRoot), "transactional"),
|
|
4812
6209
|
timeoutMs: transactional.timeoutMs,
|
|
4813
6210
|
predicted,
|
|
4814
6211
|
diffContext: {
|
|
@@ -4859,8 +6256,8 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
4859
6256
|
predictedAssessment: auditExtras.predictedAssessment,
|
|
4860
6257
|
observedAssessment: auditExtras.observedAssessment,
|
|
4861
6258
|
mode: ctx.config.mode,
|
|
4862
|
-
schemaVersion: result.
|
|
4863
|
-
...result.
|
|
6259
|
+
schemaVersion: result.axes ? 2 : 1,
|
|
6260
|
+
...result.axes ?? {},
|
|
4864
6261
|
...auditExtras.transactionalLayer
|
|
4865
6262
|
};
|
|
4866
6263
|
if (result.reason === TRANSACTIONAL_ALREADY_APPLIED) {
|
|
@@ -4883,7 +6280,17 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
4883
6280
|
});
|
|
4884
6281
|
}
|
|
4885
6282
|
const brokerActive = isCapabilityBrokerDemotionActive(ctx.config);
|
|
4886
|
-
|
|
6283
|
+
let approved = null;
|
|
6284
|
+
if (!TRANSACTIONAL_APPROVAL_BYPASS_REASONS.has(result.reason) && !shouldSkipBrokerApprovedOnce(brokerActive, result.reason)) {
|
|
6285
|
+
const approvedState = await deps.loadApprovals(ctx, "approved-approvals.json");
|
|
6286
|
+
approvedState.state = compactApprovals(approvedState.state);
|
|
6287
|
+
const matchedApproval = approvedState.state.approvals.find(
|
|
6288
|
+
(entry) => entry.kind === kind && entry.fingerprint === result.fingerprint && entry.repoRoot === ctx.repoRoot
|
|
6289
|
+
);
|
|
6290
|
+
if (!shouldSkipBrokerApprovedRecord(brokerActive, matchedApproval?.reason)) {
|
|
6291
|
+
approved = await consumeApprovedApproval(ctx, deps, kind, result.fingerprint);
|
|
6292
|
+
}
|
|
6293
|
+
}
|
|
4887
6294
|
if (approved) {
|
|
4888
6295
|
await deps.appendAudit(ctx, {
|
|
4889
6296
|
...gateBase,
|
|
@@ -5061,18 +6468,18 @@ async function appendObservedAudit(ctx, deps, eventName, payload) {
|
|
|
5061
6468
|
|
|
5062
6469
|
// src/adapters/shared/repo-root.ts
|
|
5063
6470
|
import { existsSync as existsSync7 } from "node:fs";
|
|
5064
|
-
import
|
|
6471
|
+
import path21 from "node:path";
|
|
5065
6472
|
function findRepoRoot(startPath, layout) {
|
|
5066
|
-
let current =
|
|
6473
|
+
let current = path21.resolve(startPath);
|
|
5067
6474
|
while (true) {
|
|
5068
6475
|
for (const marker of layout.repoRootMarkers) {
|
|
5069
|
-
if (existsSync7(
|
|
6476
|
+
if (existsSync7(path21.join(current, marker))) {
|
|
5070
6477
|
return current;
|
|
5071
6478
|
}
|
|
5072
6479
|
}
|
|
5073
|
-
const parent =
|
|
6480
|
+
const parent = path21.dirname(current);
|
|
5074
6481
|
if (parent === current) {
|
|
5075
|
-
return
|
|
6482
|
+
return path21.resolve(startPath);
|
|
5076
6483
|
}
|
|
5077
6484
|
current = parent;
|
|
5078
6485
|
}
|