@guilz-dev/belay 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -12
- package/dist/adapters/shared/gate-runtime.js +13 -5
- package/dist/bundle/claude-runtime.mjs +1659 -371
- package/dist/bundle/codex-runtime.mjs +1658 -371
- package/dist/bundle/cursor-runtime.mjs +1659 -371
- package/dist/cli.js +198 -8
- package/dist/commands/approve.js +11 -0
- package/dist/commands/config.d.ts +70 -0
- package/dist/commands/config.js +494 -0
- package/dist/commands/doctor.js +2 -1
- package/dist/commands/health-snapshot.d.ts +6 -0
- package/dist/commands/health-snapshot.js +34 -0
- package/dist/commands/judge.d.ts +90 -0
- package/dist/commands/judge.js +285 -0
- package/dist/commands/status.js +2 -2
- package/dist/commands/stdin-key.d.ts +1 -0
- package/dist/commands/stdin-key.js +8 -0
- package/dist/conformance/guarantee-table.js +12 -0
- package/dist/conformance/types.d.ts +2 -0
- package/dist/core/audit-io.d.ts +2 -0
- package/dist/core/audit-io.js +14 -0
- package/dist/core/capability/index.d.ts +2 -2
- package/dist/core/capability/index.js +2 -2
- package/dist/core/capability/paths.d.ts +1 -0
- package/dist/core/capability/paths.js +74 -6
- package/dist/core/capability/reasons.d.ts +3 -0
- package/dist/core/capability/reasons.js +8 -0
- package/dist/core/classify-tool.js +97 -27
- package/dist/core/config-layers.js +2 -1
- package/dist/core/config.d.ts +22 -2
- package/dist/core/config.js +110 -11
- package/dist/core/credential-store.d.ts +11 -0
- package/dist/core/credential-store.js +60 -0
- package/dist/core/gate-engine.js +104 -13
- package/dist/core/integrity.d.ts +2 -0
- package/dist/core/integrity.js +13 -0
- package/dist/core/judge-api-key.d.ts +19 -0
- package/dist/core/judge-api-key.js +74 -0
- package/dist/core/judge-cloud-consent.d.ts +13 -0
- package/dist/core/judge-cloud-consent.js +38 -0
- package/dist/core/judge-config.d.ts +41 -4
- package/dist/core/judge-config.js +263 -57
- package/dist/core/judge-doctor.d.ts +6 -1
- package/dist/core/judge-doctor.js +147 -96
- package/dist/core/judge-model-discovery.d.ts +24 -0
- package/dist/core/judge-model-discovery.js +168 -0
- package/dist/core/judge-model-policy.d.ts +5 -0
- package/dist/core/judge-model-policy.js +21 -0
- package/dist/core/judge-runtime-detection.d.ts +9 -0
- package/dist/core/judge-runtime-detection.js +68 -0
- package/dist/core/transactional/diff-evaluator.js +1 -19
- package/dist/core/types.d.ts +2 -0
- package/dist/core/verdict/adapter.d.ts +1 -0
- package/dist/core/verdict/adapter.js +7 -1
- package/dist/core/verdict/containment.d.ts +5 -0
- package/dist/core/verdict/containment.js +32 -2
- package/dist/core/verdict/judge-catalog.d.ts +40 -0
- package/dist/core/verdict/judge-catalog.js +148 -0
- package/dist/core/verdict/judge-cli.d.ts +23 -0
- package/dist/core/verdict/judge-cli.js +280 -0
- package/dist/core/verdict/judge-factory.d.ts +15 -4
- package/dist/core/verdict/judge-factory.js +117 -14
- package/dist/core/verdict/judge.d.ts +20 -1
- package/dist/core/verdict/judge.js +83 -14
- package/dist/core/verdict/persistent-paths.d.ts +8 -0
- package/dist/core/verdict/persistent-paths.js +52 -0
- package/dist/core/verdict/types.d.ts +6 -2
- package/dist/core/verdict/verdict.js +161 -47
- package/dist/installer.js +66 -15
- package/dist/types.d.ts +7 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/belay/SKILL.md +8 -7
- package/dist/commands/init-wizard.d.ts +0 -21
- package/dist/commands/init-wizard.js +0 -63
|
@@ -1,35 +1,4 @@
|
|
|
1
1
|
// agent-belay 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";
|
|
@@ -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
|
|
@@ -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,8 +946,8 @@ 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
|
|
748
953
|
var APPROVAL_EXECUTION_LEASE_MS = 6e4;
|
|
@@ -1013,6 +1218,9 @@ function isCapabilityBrokerDemotionActive(config) {
|
|
|
1013
1218
|
return hasSandboxRuntime(config);
|
|
1014
1219
|
}
|
|
1015
1220
|
|
|
1221
|
+
// src/core/capability/paths.ts
|
|
1222
|
+
import path7 from "node:path";
|
|
1223
|
+
|
|
1016
1224
|
// src/core/shell-tokenizer.ts
|
|
1017
1225
|
function tokenizeShell(input) {
|
|
1018
1226
|
const tokens = [];
|
|
@@ -1110,33 +1318,106 @@ function extractRedirectTargets(tokens) {
|
|
|
1110
1318
|
}
|
|
1111
1319
|
|
|
1112
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
|
+
}
|
|
1113
1368
|
function collectOutsideRepoPaths(command, cwd, repoRoot) {
|
|
1114
1369
|
const tokens = tokenizeShell(command);
|
|
1115
1370
|
const redirects = extractRedirectTargets(tokens);
|
|
1116
1371
|
const paths = /* @__PURE__ */ new Set();
|
|
1117
1372
|
for (const token of tokens.slice(1)) {
|
|
1118
|
-
|
|
1119
|
-
if (resolved && relativeWithinRepo(repoRoot, resolved) === null) {
|
|
1120
|
-
paths.add(resolved);
|
|
1121
|
-
}
|
|
1373
|
+
addOutsideRepoPath(paths, token, cwd, repoRoot);
|
|
1122
1374
|
}
|
|
1123
1375
|
for (const redirect of redirects) {
|
|
1124
|
-
|
|
1125
|
-
|
|
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) {
|
|
1126
1387
|
paths.add(resolved);
|
|
1127
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
|
+
}
|
|
1128
1401
|
}
|
|
1129
1402
|
return [...paths];
|
|
1130
1403
|
}
|
|
1131
1404
|
|
|
1132
1405
|
// src/core/capability/reasons.ts
|
|
1133
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
|
+
]);
|
|
1134
1412
|
function shouldSkipBrokerApprovedOnce(brokerActive, reason) {
|
|
1135
1413
|
return brokerActive && FS_SCOPE_REASONS.has(reason);
|
|
1136
1414
|
}
|
|
1415
|
+
function shouldSkipBrokerApprovedRecord(brokerActive, approvalReason) {
|
|
1416
|
+
return brokerActive && approvalReason !== void 0 && FS_SCOPE_REASONS.has(approvalReason);
|
|
1417
|
+
}
|
|
1137
1418
|
|
|
1138
1419
|
// src/core/config-layers.ts
|
|
1139
|
-
import
|
|
1420
|
+
import path8 from "node:path";
|
|
1140
1421
|
|
|
1141
1422
|
// src/presets.ts
|
|
1142
1423
|
var CONFIG_PRESETS = {
|
|
@@ -1236,8 +1517,8 @@ function applyConfigPreset(preset, extra = {}) {
|
|
|
1236
1517
|
// src/core/config-layers.ts
|
|
1237
1518
|
function teamConfigPath(homedir = () => process.env.HOME ?? process.env.USERPROFILE ?? "") {
|
|
1238
1519
|
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
1239
|
-
const base = xdg ||
|
|
1240
|
-
return
|
|
1520
|
+
const base = xdg || path8.join(homedir(), ".config");
|
|
1521
|
+
return path8.join(base, "agent-belay", "team.config.json");
|
|
1241
1522
|
}
|
|
1242
1523
|
function applyProtectedLayer(config, builtin) {
|
|
1243
1524
|
const controlPlane = { ...config.controlPlane };
|
|
@@ -1271,6 +1552,10 @@ function resolveLayeredConfig(params) {
|
|
|
1271
1552
|
if (params.teamConfig) {
|
|
1272
1553
|
const teamFile = params.teamConfig;
|
|
1273
1554
|
const teamRaw = teamFile.preset ? applyConfigPreset(teamFile.preset, teamFile.config ?? {}) : teamFile.config ?? params.teamConfig;
|
|
1555
|
+
rejectTeamLayerJudgeSecrets(
|
|
1556
|
+
teamRaw.judge,
|
|
1557
|
+
"team"
|
|
1558
|
+
);
|
|
1274
1559
|
config = mergeConfigLayer(config, asV3Layer(teamRaw));
|
|
1275
1560
|
provenance.push({
|
|
1276
1561
|
path: params.teamConfigPath ?? teamConfigPath(),
|
|
@@ -1474,7 +1759,7 @@ function classifySubagent(payload, repoRoot, options = {}) {
|
|
|
1474
1759
|
}
|
|
1475
1760
|
|
|
1476
1761
|
// src/core/classify-tool.ts
|
|
1477
|
-
import
|
|
1762
|
+
import path16 from "node:path";
|
|
1478
1763
|
|
|
1479
1764
|
// src/core/glob.ts
|
|
1480
1765
|
function matchesSensitivePath(filePath, patterns) {
|
|
@@ -1534,62 +1819,472 @@ function judgeTraceAuditFields(trace) {
|
|
|
1534
1819
|
};
|
|
1535
1820
|
}
|
|
1536
1821
|
|
|
1537
|
-
// src/
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
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
|
+
};
|
|
1548
1860
|
}
|
|
1549
|
-
|
|
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);
|
|
1550
1868
|
}
|
|
1551
|
-
|
|
1552
|
-
|
|
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];
|
|
1553
1911
|
}
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
return true;
|
|
1560
|
-
}
|
|
1912
|
+
|
|
1913
|
+
// src/config-io.ts
|
|
1914
|
+
function resolveAdapterName(config) {
|
|
1915
|
+
if (config.adapter === "claude") {
|
|
1916
|
+
return "claude";
|
|
1561
1917
|
}
|
|
1562
|
-
|
|
1918
|
+
if (config.adapter === "codex") {
|
|
1919
|
+
return "codex";
|
|
1920
|
+
}
|
|
1921
|
+
return "cursor";
|
|
1563
1922
|
}
|
|
1564
|
-
function
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
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);
|
|
1568
1978
|
return true;
|
|
1979
|
+
} catch {
|
|
1569
1980
|
}
|
|
1570
1981
|
}
|
|
1571
1982
|
return false;
|
|
1572
1983
|
}
|
|
1573
|
-
function
|
|
1574
|
-
const
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
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 };
|
|
1578
1988
|
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
if (segments.length > 1) {
|
|
1582
|
-
return token.replace(basename, "[REDACTED]");
|
|
1989
|
+
if (env.BELAY_JUDGE_DISABLE_CLI_TRANSPORT === "1") {
|
|
1990
|
+
return { http: true, cliTransport: null };
|
|
1583
1991
|
}
|
|
1584
|
-
|
|
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 };
|
|
1585
2000
|
}
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
}
|
|
1592
|
-
|
|
2001
|
+
|
|
2002
|
+
// src/core/judge-api-key.ts
|
|
2003
|
+
function resolveFromEnvChain(env, catalogSpec) {
|
|
2004
|
+
const belay = env.BELAY_JUDGE_API_KEY?.trim();
|
|
2005
|
+
if (belay) {
|
|
2006
|
+
return { key: belay, source: "BELAY_JUDGE_API_KEY" };
|
|
2007
|
+
}
|
|
2008
|
+
for (const name of catalogSpec?.apiKeyEnvVars ?? ["OPENAI_API_KEY", "BELAY_JUDGE_API_KEY"]) {
|
|
2009
|
+
const value = env[name]?.trim();
|
|
2010
|
+
if (value) {
|
|
2011
|
+
return { key: value, source: name };
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
return { key: null, source: null };
|
|
2015
|
+
}
|
|
2016
|
+
function sourceKindForProjectMode(fromEnv, judge, catalogSpec) {
|
|
2017
|
+
if (fromEnv.key) {
|
|
2018
|
+
return "env";
|
|
2019
|
+
}
|
|
2020
|
+
const providerId = judge.providerId && normalizeLegacyProviderId(judge.providerId) ? normalizeLegacyProviderId(judge.providerId) : inferProviderIdFromConfig(judge);
|
|
2021
|
+
const spec = catalogSpec ?? getJudgeProviderSpec(providerId);
|
|
2022
|
+
if (spec?.isCloud && detectJudgeRuntimeCapabilities(providerId).cliTransport) {
|
|
2023
|
+
return "host-session";
|
|
2024
|
+
}
|
|
2025
|
+
return "missing";
|
|
2026
|
+
}
|
|
2027
|
+
function sourceKindForApiKeyMode(key, source) {
|
|
2028
|
+
if (!key) {
|
|
2029
|
+
return "missing";
|
|
2030
|
+
}
|
|
2031
|
+
if (source?.startsWith("env:")) {
|
|
2032
|
+
return "env";
|
|
2033
|
+
}
|
|
2034
|
+
return "store";
|
|
2035
|
+
}
|
|
2036
|
+
async function resolveJudgeCredential(params) {
|
|
2037
|
+
const env = params.env ?? process.env;
|
|
2038
|
+
const credential = params.judge.credential ?? { mode: "project" };
|
|
2039
|
+
if (credential.mode === "apiKey") {
|
|
2040
|
+
const ref = credential.ref ? parseCredentialRef(credential.ref) : "store:judge";
|
|
2041
|
+
if (ref === "store:judge") {
|
|
2042
|
+
const stateDir = belayStateDir(params.config, params.repoLocalStateDir);
|
|
2043
|
+
const key = await readJudgeCredentialStore(stateDir);
|
|
2044
|
+
return {
|
|
2045
|
+
key,
|
|
2046
|
+
source: key ? `${stateDir}/credentials.json` : null,
|
|
2047
|
+
sourceKind: sourceKindForApiKeyMode(key, key ? `${stateDir}/credentials.json` : null),
|
|
2048
|
+
mode: "apiKey"
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
if (ref?.startsWith("env:")) {
|
|
2052
|
+
const envName = ref.slice(4);
|
|
2053
|
+
const value = env[envName]?.trim() ?? null;
|
|
2054
|
+
return {
|
|
2055
|
+
key: value,
|
|
2056
|
+
source: value ? `env:${envName}` : null,
|
|
2057
|
+
sourceKind: sourceKindForApiKeyMode(value, value ? `env:${envName}` : null),
|
|
2058
|
+
mode: "apiKey"
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
return { key: null, source: null, sourceKind: "missing", mode: "apiKey" };
|
|
2062
|
+
}
|
|
2063
|
+
const fromEnv = resolveFromEnvChain(env, params.catalogSpec);
|
|
2064
|
+
return {
|
|
2065
|
+
...fromEnv,
|
|
2066
|
+
sourceKind: sourceKindForProjectMode(fromEnv, params.judge, params.catalogSpec),
|
|
2067
|
+
mode: "project"
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
// src/core/judge-config.ts
|
|
2072
|
+
var JUDGE_PROFILE_LOCAL_OLLAMA = resolveJudgeFromCatalog({
|
|
2073
|
+
providerId: "ollama"
|
|
2074
|
+
});
|
|
2075
|
+
var JUDGE_PROFILE_CURSOR = resolveJudgeFromCatalog({
|
|
2076
|
+
providerId: "cursor"
|
|
2077
|
+
});
|
|
2078
|
+
var JUDGE_PROFILE_CLAUDE = resolveJudgeFromCatalog({
|
|
2079
|
+
providerId: "claude"
|
|
2080
|
+
});
|
|
2081
|
+
var JUDGE_PROFILE_CODEX = resolveJudgeFromCatalog({
|
|
2082
|
+
providerId: "codex"
|
|
2083
|
+
});
|
|
2084
|
+
function isCloudJudgeConfig(judge) {
|
|
2085
|
+
if (judge.providerId) {
|
|
2086
|
+
return isCloudProviderId(judge.providerId);
|
|
2087
|
+
}
|
|
2088
|
+
return judge.provider === "openai-compatible" || judge.provider === "anthropic";
|
|
2089
|
+
}
|
|
2090
|
+
function hasValidCloudConsent(judge) {
|
|
2091
|
+
if (!judge.cloudConsent?.accepted) {
|
|
2092
|
+
return false;
|
|
2093
|
+
}
|
|
2094
|
+
if (!judge.endpoint?.trim()) {
|
|
2095
|
+
return false;
|
|
2096
|
+
}
|
|
2097
|
+
const providerId = judge.providerId && normalizeLegacyProviderId(judge.providerId) ? normalizeLegacyProviderId(judge.providerId) : judge.provider === "ollama" ? "ollama" : "codex";
|
|
2098
|
+
return judge.cloudConsent.endpoint === judge.endpoint.trim() && normalizeLegacyProviderId(judge.cloudConsent.providerId) === providerId;
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
// src/core/verdict/containment.ts
|
|
2102
|
+
import path13 from "node:path";
|
|
2103
|
+
|
|
2104
|
+
// src/core/verdict/persistent-paths.ts
|
|
2105
|
+
function isPersistentAgentPath(resolvedPath) {
|
|
2106
|
+
const normalized = resolvedPath.replaceAll("\\", "/");
|
|
2107
|
+
return /\.ssh\/authorized_keys$/i.test(normalized) || /\/\.(zshrc|bashrc|bash_profile|profile)$/i.test(normalized) || /\/\.config\//i.test(normalized) || /\/crontab$/i.test(normalized) || /LaunchAgents\//i.test(normalized) || /\/Library\/LaunchDaemons\//i.test(normalized);
|
|
2108
|
+
}
|
|
2109
|
+
function isOutsideRepoSecretCredentialPath(resolvedPath) {
|
|
2110
|
+
const normalized = resolvedPath.replaceAll("\\", "/");
|
|
2111
|
+
return /\/\.env(?:\.[^/]+)?$/i.test(normalized) || /\/[^/]+\.pem$/i.test(normalized) || /\/\.ssh\/id_[^/]+$/i.test(normalized) || /\/\.aws\//i.test(normalized) || /\/\.npmrc$/i.test(normalized) || /\/\.git-credentials$/i.test(normalized) || /\/\.netrc$/i.test(normalized) || /\/\.kube\/config$/i.test(normalized) || /\/\.docker\/config\.json$/i.test(normalized) || /\/\.gnupg\//i.test(normalized) || /\/\.pypirc$/i.test(normalized);
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
// src/core/verdict/containment.ts
|
|
2115
|
+
function expandHome(token) {
|
|
2116
|
+
if (token === "~" || token.startsWith("~/")) {
|
|
2117
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
2118
|
+
if (!home) {
|
|
2119
|
+
return token;
|
|
2120
|
+
}
|
|
2121
|
+
return token === "~" ? home : path13.join(home, token.slice(2));
|
|
2122
|
+
}
|
|
2123
|
+
return token;
|
|
2124
|
+
}
|
|
2125
|
+
function resolveTrustedPath(token, trustedCwd, trusted) {
|
|
2126
|
+
if (!token || token === "--" || token.startsWith("-")) {
|
|
2127
|
+
return null;
|
|
2128
|
+
}
|
|
2129
|
+
if (!trusted || !trustedCwd) {
|
|
2130
|
+
return null;
|
|
2131
|
+
}
|
|
2132
|
+
const expanded = expandHome(token);
|
|
2133
|
+
if (path13.isAbsolute(expanded)) {
|
|
2134
|
+
return canonicalPath(expanded);
|
|
2135
|
+
}
|
|
2136
|
+
return canonicalPath(path13.resolve(trustedCwd, expanded));
|
|
2137
|
+
}
|
|
2138
|
+
function locationForPath(resolvedPath, repoRoot) {
|
|
2139
|
+
if (!resolvedPath) {
|
|
2140
|
+
return "unknown";
|
|
2141
|
+
}
|
|
2142
|
+
if (pathWithinRoot(repoRoot, resolvedPath)) {
|
|
2143
|
+
return "repo_local";
|
|
2144
|
+
}
|
|
2145
|
+
return "repo_outside";
|
|
2146
|
+
}
|
|
2147
|
+
function isGitPath(resolvedPath, repoRoot) {
|
|
2148
|
+
const relative = relativeWithinRepo(repoRoot, resolvedPath);
|
|
2149
|
+
if (!relative) {
|
|
2150
|
+
return false;
|
|
2151
|
+
}
|
|
2152
|
+
const normalized = relative.replaceAll("\\", "/");
|
|
2153
|
+
return normalized === ".git" || normalized.startsWith(".git/");
|
|
2154
|
+
}
|
|
2155
|
+
function isHighStakesPath(resolvedPath, repoRoot, sensitivePaths, protectedRoots = []) {
|
|
2156
|
+
if (isGitPath(resolvedPath, repoRoot)) {
|
|
2157
|
+
return true;
|
|
2158
|
+
}
|
|
2159
|
+
const relative = relativeWithinRepo(repoRoot, resolvedPath);
|
|
2160
|
+
if (relative !== null && matchesSensitivePath(relative.replaceAll("\\", "/"), sensitivePaths)) {
|
|
2161
|
+
return true;
|
|
2162
|
+
}
|
|
2163
|
+
if (relative === null && isOutsideRepoSecretCredentialPath(resolvedPath)) {
|
|
2164
|
+
return true;
|
|
2165
|
+
}
|
|
2166
|
+
return protectedRoots.some((root) => pathWithinRoot(root, resolvedPath));
|
|
2167
|
+
}
|
|
2168
|
+
var DESTRUCTIVE_MUTATION_HEADS = /* @__PURE__ */ new Set([
|
|
2169
|
+
"rm",
|
|
2170
|
+
"shred",
|
|
2171
|
+
"truncate",
|
|
2172
|
+
"dd",
|
|
2173
|
+
"chmod",
|
|
2174
|
+
"chown",
|
|
2175
|
+
"mv",
|
|
2176
|
+
"cp"
|
|
2177
|
+
]);
|
|
2178
|
+
function isDestructiveMutationHead(head) {
|
|
2179
|
+
return DESTRUCTIVE_MUTATION_HEADS.has(head);
|
|
2180
|
+
}
|
|
2181
|
+
function touchesProtectedRoot(resolvedPath, protectedRoots) {
|
|
2182
|
+
return protectedRoots.some((root) => pathWithinRoot(root, resolvedPath) || root === resolvedPath);
|
|
2183
|
+
}
|
|
2184
|
+
function isDestructiveHighStakesMutation(head, resolvedPath, repoRoot, sensitivePaths, protectedRoots = []) {
|
|
2185
|
+
if (touchesProtectedRoot(resolvedPath, protectedRoots)) {
|
|
2186
|
+
return true;
|
|
2187
|
+
}
|
|
2188
|
+
if (!isDestructiveMutationHead(head)) {
|
|
2189
|
+
return false;
|
|
2190
|
+
}
|
|
2191
|
+
return isHighStakesPath(resolvedPath, repoRoot, sensitivePaths, []);
|
|
2192
|
+
}
|
|
2193
|
+
function analyzePathTargets(params) {
|
|
2194
|
+
const signals = [];
|
|
2195
|
+
if (!params.trustedCwd || !params.cwd) {
|
|
2196
|
+
return {
|
|
2197
|
+
location: "unknown",
|
|
2198
|
+
isHighStakes: false,
|
|
2199
|
+
signals: ["missing_trusted_cwd"]
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
const locations = /* @__PURE__ */ new Set();
|
|
2203
|
+
let isHighStakes = false;
|
|
2204
|
+
for (const target of params.targets) {
|
|
2205
|
+
const resolved = resolveTrustedPath(target, params.cwd, params.trustedCwd) ?? resolveMutationTarget(target, params.cwd);
|
|
2206
|
+
const location2 = locationForPath(resolved, params.repoRoot);
|
|
2207
|
+
locations.add(location2);
|
|
2208
|
+
if (resolved && isHighStakesPath(
|
|
2209
|
+
resolved,
|
|
2210
|
+
params.repoRoot,
|
|
2211
|
+
params.sensitivePaths,
|
|
2212
|
+
params.protectedArtifactRoots
|
|
2213
|
+
)) {
|
|
2214
|
+
isHighStakes = true;
|
|
2215
|
+
signals.push("high_stakes_path");
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
let location = "unknown";
|
|
2219
|
+
if (locations.size === 0) {
|
|
2220
|
+
location = "unknown";
|
|
2221
|
+
} else if (locations.size === 1) {
|
|
2222
|
+
location = [...locations][0] ?? "unknown";
|
|
2223
|
+
} else {
|
|
2224
|
+
location = "mixed";
|
|
2225
|
+
}
|
|
2226
|
+
return { location, isHighStakes, signals };
|
|
2227
|
+
}
|
|
2228
|
+
function cwdRelative(repoRoot, cwd) {
|
|
2229
|
+
return relativeWithinRepo(repoRoot, cwd) ?? cwd;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
// src/core/verdict/judge-outbound.ts
|
|
2233
|
+
var PATH_LIKE = /(?:^|[\s"'`=])(~\/[^\s"'`]+|\/[^\s"'`]+|\.\/[^\s"'`]+|\.\.\/[^\s"'`]+|[A-Za-z]:\\[^\s"'`]+)/g;
|
|
2234
|
+
var REDACTED_PLACEHOLDER = /^(?:<redacted>|\[REDACTED\]|<secret>|<high-entropy>|<approval-id>)$/i;
|
|
2235
|
+
var URL_CREDENTIALS_PATTERN2 = /\b[A-Za-z][A-Za-z0-9+.-]*:\/\/([^/\s:@]+):([^@\s/]+)@/gi;
|
|
2236
|
+
var GENERIC_AUTH_HEADER_PATTERN2 = /\b(?:Authorization|X-Api-Key|X-Auth-Token|Private-Token):\s*([^\s]+)/gi;
|
|
2237
|
+
function hasResidualBearerToken(text) {
|
|
2238
|
+
for (const match of text.matchAll(/\bBearer\s+(\S+)/gi)) {
|
|
2239
|
+
const token = match[1] ?? "";
|
|
2240
|
+
if (!REDACTED_PLACEHOLDER.test(token)) {
|
|
2241
|
+
return true;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
return false;
|
|
2245
|
+
}
|
|
2246
|
+
function hasResidualApiKey(text) {
|
|
2247
|
+
return /\bsk-(?![^\s]*<redacted>)[A-Za-z0-9_-]{4,}/i.test(text);
|
|
2248
|
+
}
|
|
2249
|
+
function hasResidualUrlCredentials(text) {
|
|
2250
|
+
for (const match of text.matchAll(URL_CREDENTIALS_PATTERN2)) {
|
|
2251
|
+
const username = (match[1] ?? "").replace(/^['"]|['"]$/g, "");
|
|
2252
|
+
const password = (match[2] ?? "").replace(/^['"]|['"]$/g, "");
|
|
2253
|
+
if (!REDACTED_PLACEHOLDER.test(username) || !REDACTED_PLACEHOLDER.test(password)) {
|
|
2254
|
+
return true;
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
return false;
|
|
2258
|
+
}
|
|
2259
|
+
function hasResidualAuthHeader(text) {
|
|
2260
|
+
for (const match of text.matchAll(GENERIC_AUTH_HEADER_PATTERN2)) {
|
|
2261
|
+
const token = (match[1] ?? "").replace(/^['"]|['"]$/g, "");
|
|
2262
|
+
if (!REDACTED_PLACEHOLDER.test(token)) {
|
|
2263
|
+
return true;
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
return false;
|
|
2267
|
+
}
|
|
2268
|
+
function redactSensitivePathToken(token, sensitivePaths) {
|
|
2269
|
+
const trimmed = token.replace(/^['"`]+|['"`]+$/g, "");
|
|
2270
|
+
const normalized = trimmed.replaceAll("\\", "/");
|
|
2271
|
+
if (!matchesSensitivePath(normalized, sensitivePaths)) {
|
|
2272
|
+
return token;
|
|
2273
|
+
}
|
|
2274
|
+
const segments = normalized.split("/");
|
|
2275
|
+
const basename = segments.at(-1) ?? normalized;
|
|
2276
|
+
if (segments.length > 1) {
|
|
2277
|
+
return token.replace(basename, "[REDACTED]");
|
|
2278
|
+
}
|
|
2279
|
+
return "[REDACTED]";
|
|
2280
|
+
}
|
|
2281
|
+
function scrubOutboundForJudge(text, options) {
|
|
2282
|
+
try {
|
|
2283
|
+
let scrubbed = scrubString(text, {
|
|
2284
|
+
...options.scrubOptions,
|
|
2285
|
+
maskHighEntropyStrings: options.scrubOptions.maskHighEntropyStrings !== false
|
|
2286
|
+
});
|
|
2287
|
+
scrubbed = scrubbed.replace(PATH_LIKE, (match, pathToken) => {
|
|
1593
2288
|
const redacted = redactSensitivePathToken(pathToken, options.sensitivePaths);
|
|
1594
2289
|
return match.replace(pathToken, redacted);
|
|
1595
2290
|
});
|
|
@@ -1607,33 +2302,53 @@ function scrubOutboundForJudge(text, options) {
|
|
|
1607
2302
|
var SECRET_PATTERNS = [/\.env\b/i, /\.pem\b/i, /id_rsa\b/i, /credentials/i, /secrets?\b/i];
|
|
1608
2303
|
var DESTRUCTIVE_VERBS = /\b(rm|rmtree|unlink|delete|truncate|shred|destroy|drop)\b/i;
|
|
1609
2304
|
var GIT_PATTERNS = /\.git\b/i;
|
|
1610
|
-
var TIER1_PROMPT = `Answer ONLY with JSON: {"
|
|
2305
|
+
var TIER1_PROMPT = `Answer ONLY with JSON: {"local_recoverable":boolean,"destroys_history_or_secrets":boolean,"reason":string}
|
|
1611
2306
|
reason should be a short snake_case label.
|
|
1612
2307
|
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
2308
|
+
local_recoverable: true if a mistaken operation can be undone locally (git revert, fs snapshot, trivial manual revert).
|
|
2309
|
+
local_recoverable: false if remote/destructive (git push --force, aws s3 rm, secret send) OR persistent local harm (~/.ssh/authorized_keys, shell rc, crontab, ~/.config persistence).
|
|
2310
|
+
|
|
2311
|
+
destroys_history_or_secrets: true if the operation deletes or corrupts git history (.git), overwrites secrets/credentials, or modifies persistent agent startup (shell rc, crontab, launchd, authorized_keys).
|
|
2312
|
+
|
|
2313
|
+
Examples local_recoverable=true, destroys_history_or_secrets=false: IDE plan files under the user home, /tmp files, in-repo edits, READ/QUERY/GET.
|
|
2314
|
+
Examples local_recoverable=false OR destroys_history_or_secrets=true: remote mutations, credential paths, agent startup persistence, rm -rf .git.
|
|
2315
|
+
If genuinely ambiguous about recoverability or persistent harm, answer false.
|
|
1616
2316
|
|
|
1617
2317
|
Command/code:
|
|
1618
2318
|
`;
|
|
1619
2319
|
function failClosedVerdict(reason) {
|
|
1620
2320
|
return {
|
|
1621
|
-
|
|
2321
|
+
local_recoverable: false,
|
|
1622
2322
|
destroys_outside_repo: true,
|
|
1623
2323
|
destroys_history_or_secrets: true,
|
|
2324
|
+
external_change: true,
|
|
1624
2325
|
reason
|
|
1625
2326
|
};
|
|
1626
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
|
+
}
|
|
1627
2340
|
function parseTier1Json(raw) {
|
|
1628
2341
|
try {
|
|
1629
2342
|
const parsed = JSON.parse(raw);
|
|
1630
|
-
|
|
2343
|
+
const localRecoverable = parseLocalRecoverable(parsed);
|
|
2344
|
+
if (localRecoverable === null) {
|
|
1631
2345
|
return null;
|
|
1632
2346
|
}
|
|
1633
2347
|
return {
|
|
2348
|
+
local_recoverable: localRecoverable,
|
|
2349
|
+
destroys_outside_repo: typeof parsed.destroys_outside_repo === "boolean" ? parsed.destroys_outside_repo : false,
|
|
2350
|
+
destroys_history_or_secrets: typeof parsed.destroys_history_or_secrets === "boolean" ? parsed.destroys_history_or_secrets : false,
|
|
1634
2351
|
external_change: parsed.external_change,
|
|
1635
|
-
destroys_outside_repo: false,
|
|
1636
|
-
destroys_history_or_secrets: false,
|
|
1637
2352
|
reason: typeof parsed.reason === "string" ? parsed.reason : "tier1_llm"
|
|
1638
2353
|
};
|
|
1639
2354
|
} catch {
|
|
@@ -1647,7 +2362,7 @@ function prescanInterpreterCode(code) {
|
|
|
1647
2362
|
const hitsDestructive = DESTRUCTIVE_VERBS.test(normalized);
|
|
1648
2363
|
if ((hitsSecret || hitsGit) && hitsDestructive) {
|
|
1649
2364
|
return {
|
|
1650
|
-
|
|
2365
|
+
local_recoverable: true,
|
|
1651
2366
|
destroys_outside_repo: false,
|
|
1652
2367
|
destroys_history_or_secrets: true,
|
|
1653
2368
|
reason: "prescan_destructive_secret"
|
|
@@ -1655,11 +2370,49 @@ function prescanInterpreterCode(code) {
|
|
|
1655
2370
|
}
|
|
1656
2371
|
return null;
|
|
1657
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
|
+
}
|
|
1658
2411
|
function createDeterministicJudgeStub() {
|
|
1659
2412
|
return {
|
|
1660
2413
|
evaluate() {
|
|
1661
2414
|
return Promise.resolve({
|
|
1662
|
-
|
|
2415
|
+
local_recoverable: true,
|
|
1663
2416
|
destroys_outside_repo: false,
|
|
1664
2417
|
destroys_history_or_secrets: false,
|
|
1665
2418
|
reason: "deterministic_stub"
|
|
@@ -1791,8 +2544,7 @@ function createOpenAiCompatibleJudge(options) {
|
|
|
1791
2544
|
};
|
|
1792
2545
|
return failClosedVerdict("outbound_scrub_failed");
|
|
1793
2546
|
}
|
|
1794
|
-
const
|
|
1795
|
-
const resolvedKey = resolveJudgeApiKey2();
|
|
2547
|
+
const resolvedKey = options.resolveApiKey ? await options.resolveApiKey() : { key: options.apiKey ?? null, source: null };
|
|
1796
2548
|
const apiKey = options.apiKey ?? resolvedKey.key;
|
|
1797
2549
|
if (!apiKey) {
|
|
1798
2550
|
judge.lastTrace = {
|
|
@@ -1856,149 +2608,418 @@ function createOpenAiCompatibleJudge(options) {
|
|
|
1856
2608
|
return judge;
|
|
1857
2609
|
}
|
|
1858
2610
|
function tier1RequiresAsk(verdict2) {
|
|
1859
|
-
return verdict2.
|
|
2611
|
+
return !verdict2.local_recoverable || verdict2.destroys_history_or_secrets;
|
|
1860
2612
|
}
|
|
1861
2613
|
|
|
1862
|
-
// src/core/verdict/judge-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
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") {
|
|
1867
2650
|
return {
|
|
1868
|
-
|
|
1869
|
-
|
|
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
|
|
1870
2663
|
};
|
|
1871
2664
|
}
|
|
1872
|
-
|
|
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
|
+
};
|
|
1873
2699
|
}
|
|
1874
|
-
function
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
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
|
+
}
|
|
1886
2717
|
}
|
|
1887
|
-
|
|
1888
|
-
const { resolved } = resolveCloudModel(judgeConfig.model, pinned);
|
|
1889
|
-
return createOpenAiCompatibleJudge({
|
|
1890
|
-
endpoint,
|
|
1891
|
-
modelRequested: judgeConfig.model,
|
|
1892
|
-
modelResolved: resolved,
|
|
1893
|
-
timeoutMs: judgeConfig.timeoutMs,
|
|
1894
|
-
sensitivePaths: config.classifier.sensitivePaths,
|
|
1895
|
-
scrubOptions: scrubOptionsFromConfig(config)
|
|
1896
|
-
});
|
|
2718
|
+
return null;
|
|
1897
2719
|
}
|
|
1898
|
-
if (
|
|
1899
|
-
return
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
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
|
+
}
|
|
1905
2732
|
}
|
|
1906
|
-
|
|
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;
|
|
1907
2740
|
}
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
return token;
|
|
2741
|
+
function parseCliJsonLines(raw) {
|
|
2742
|
+
const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
2743
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
2744
|
+
const parsed = tryParseJson(lines[index] ?? "");
|
|
2745
|
+
if (!parsed) {
|
|
2746
|
+
continue;
|
|
2747
|
+
}
|
|
2748
|
+
const verdict2 = extractVerdictFromJsonValue(parsed);
|
|
2749
|
+
if (verdict2) {
|
|
2750
|
+
return verdict2;
|
|
1919
2751
|
}
|
|
1920
|
-
return token === "~" ? home : path8.join(home, token.slice(2));
|
|
1921
2752
|
}
|
|
1922
|
-
return
|
|
2753
|
+
return null;
|
|
1923
2754
|
}
|
|
1924
|
-
function
|
|
1925
|
-
|
|
1926
|
-
|
|
2755
|
+
function parseCliJudgeOutput(providerId, raw) {
|
|
2756
|
+
const direct = parseTier1Json(raw);
|
|
2757
|
+
if (direct) {
|
|
2758
|
+
return direct;
|
|
1927
2759
|
}
|
|
1928
|
-
|
|
1929
|
-
|
|
2760
|
+
const parsed = tryParseJson(raw);
|
|
2761
|
+
if (parsed) {
|
|
2762
|
+
const envelopeVerdict = extractVerdictFromJsonValue(parsed);
|
|
2763
|
+
if (envelopeVerdict) {
|
|
2764
|
+
return envelopeVerdict;
|
|
2765
|
+
}
|
|
1930
2766
|
}
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
return canonicalPath(expanded);
|
|
2767
|
+
if (providerId === "codex") {
|
|
2768
|
+
return parseCliJsonLines(raw);
|
|
1934
2769
|
}
|
|
1935
|
-
return
|
|
2770
|
+
return parseCliJsonLines(raw);
|
|
1936
2771
|
}
|
|
1937
|
-
function
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
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;
|
|
1943
2903
|
}
|
|
1944
|
-
return
|
|
2904
|
+
return process.env.BELAY_JUDGE_MODEL_RESOLVED?.trim() || void 0;
|
|
1945
2905
|
}
|
|
1946
|
-
function
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
2906
|
+
function providerIdFromJudge(judge) {
|
|
2907
|
+
if (judge.providerId) {
|
|
2908
|
+
const normalized = normalizeLegacyProviderId(judge.providerId);
|
|
2909
|
+
if (normalized) {
|
|
2910
|
+
return normalized;
|
|
2911
|
+
}
|
|
1950
2912
|
}
|
|
1951
|
-
|
|
1952
|
-
return normalized === ".git" || normalized.startsWith(".git/");
|
|
2913
|
+
return inferProviderIdFromConfig(judge);
|
|
1953
2914
|
}
|
|
1954
|
-
function
|
|
1955
|
-
|
|
1956
|
-
|
|
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);
|
|
1957
2926
|
}
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
if (matchesSensitivePath(checkPath.replaceAll("\\", "/"), sensitivePaths)) {
|
|
1961
|
-
return true;
|
|
2927
|
+
if (providerId === "cursor") {
|
|
2928
|
+
return createCursorCliJudge(options);
|
|
1962
2929
|
}
|
|
1963
|
-
return
|
|
2930
|
+
return createClaudeCliJudge(options);
|
|
1964
2931
|
}
|
|
1965
|
-
function
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
return {
|
|
1969
|
-
location: "unknown",
|
|
1970
|
-
isHighStakes: false,
|
|
1971
|
-
signals: ["missing_trusted_cwd"]
|
|
1972
|
-
};
|
|
2932
|
+
function createJudgeFromConfig(config, options = {}) {
|
|
2933
|
+
if (process.env.BELAY_DETERMINISTIC_JUDGE === "1") {
|
|
2934
|
+
return createDeterministicJudgeStub();
|
|
1973
2935
|
}
|
|
1974
|
-
const
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2936
|
+
const judgeConfig = config.judge;
|
|
2937
|
+
if (judgeConfig.providerId && isRemovedProviderId(String(judgeConfig.providerId))) {
|
|
2938
|
+
const { resolved: resolved2 } = resolveJudgeModel(judgeConfig);
|
|
2939
|
+
return createFailClosedJudge({
|
|
2940
|
+
reason: "judge_provider_removed",
|
|
2941
|
+
fallbackReason: "provider_migration_required",
|
|
2942
|
+
modelRequested: judgeConfig.model,
|
|
2943
|
+
modelResolved: resolved2
|
|
2944
|
+
});
|
|
2945
|
+
}
|
|
2946
|
+
const provider = normalizeJudgeProvider(judgeConfig.provider);
|
|
2947
|
+
const providerId = providerIdFromJudge(judgeConfig);
|
|
2948
|
+
const catalogSpec = getJudgeProviderSpec(providerId);
|
|
2949
|
+
const { resolved } = resolveJudgeModel(judgeConfig);
|
|
2950
|
+
const runtime = detectJudgeRuntimeCapabilities(providerId);
|
|
2951
|
+
if (provider === "openai-compatible") {
|
|
2952
|
+
const endpoint = judgeConfig.endpoint?.trim();
|
|
2953
|
+
if (!endpoint && runtime.cliTransport) {
|
|
2954
|
+
if (providerId === "codex" || providerId === "cursor") {
|
|
2955
|
+
return createCliJudgeForProvider(providerId, judgeConfig, config);
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
if (!endpoint) {
|
|
2959
|
+
return createFailClosedJudge({
|
|
2960
|
+
reason: "openai_compatible_endpoint_missing",
|
|
2961
|
+
fallbackReason: "missing_endpoint",
|
|
2962
|
+
modelRequested: judgeConfig.model,
|
|
2963
|
+
modelResolved: resolved
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
if (isCloudJudgeConfig(judgeConfig) && !hasValidCloudConsent(judgeConfig)) {
|
|
2967
|
+
return createFailClosedJudge({
|
|
2968
|
+
reason: "openai_compatible_consent_missing",
|
|
2969
|
+
fallbackReason: "cloud_consent_missing",
|
|
2970
|
+
modelRequested: judgeConfig.model,
|
|
2971
|
+
modelResolved: resolved
|
|
2972
|
+
});
|
|
1988
2973
|
}
|
|
2974
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
2975
|
+
const repoLocalStateDir = repoLocalStateDirFor(repoRoot, config);
|
|
2976
|
+
return createOpenAiCompatibleJudge({
|
|
2977
|
+
endpoint,
|
|
2978
|
+
modelRequested: judgeConfig.model,
|
|
2979
|
+
modelResolved: resolved,
|
|
2980
|
+
timeoutMs: judgeConfig.timeoutMs,
|
|
2981
|
+
sensitivePaths: config.classifier.sensitivePaths,
|
|
2982
|
+
scrubOptions: scrubOptionsFromConfig(config),
|
|
2983
|
+
resolveApiKey: async () => resolveJudgeCredential({
|
|
2984
|
+
judge: judgeConfig,
|
|
2985
|
+
catalogSpec: catalogSpec ?? void 0,
|
|
2986
|
+
repoRoot,
|
|
2987
|
+
repoLocalStateDir,
|
|
2988
|
+
config
|
|
2989
|
+
})
|
|
2990
|
+
});
|
|
1989
2991
|
}
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
2992
|
+
if (provider === "ollama") {
|
|
2993
|
+
return createOllamaJudge({
|
|
2994
|
+
model: judgeConfig.model,
|
|
2995
|
+
baseUrl: judgeConfig.endpoint ?? "http://127.0.0.1:11434",
|
|
2996
|
+
timeoutMs: judgeConfig.timeoutMs,
|
|
2997
|
+
keepAlive: judgeConfig.keepAlive
|
|
2998
|
+
});
|
|
1997
2999
|
}
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
3000
|
+
if (provider === "anthropic") {
|
|
3001
|
+
if (runtime.cliTransport) {
|
|
3002
|
+
return createClaudeCliJudge({
|
|
3003
|
+
modelRequested: judgeConfig.model,
|
|
3004
|
+
modelResolved: resolved,
|
|
3005
|
+
timeoutMs: judgeConfig.timeoutMs,
|
|
3006
|
+
sensitivePaths: config.classifier.sensitivePaths,
|
|
3007
|
+
scrubOptions: scrubOptionsFromConfig(config)
|
|
3008
|
+
});
|
|
3009
|
+
}
|
|
3010
|
+
return createFailClosedJudge({
|
|
3011
|
+
reason: "anthropic_not_implemented",
|
|
3012
|
+
fallbackReason: "anthropic_runtime_unavailable",
|
|
3013
|
+
modelRequested: judgeConfig.model,
|
|
3014
|
+
modelResolved: resolved
|
|
3015
|
+
});
|
|
3016
|
+
}
|
|
3017
|
+
return createFailClosedJudge({
|
|
3018
|
+
reason: "unsupported_judge_provider",
|
|
3019
|
+
fallbackReason: "unsupported_provider",
|
|
3020
|
+
modelRequested: judgeConfig.model,
|
|
3021
|
+
modelResolved: resolved
|
|
3022
|
+
});
|
|
2002
3023
|
}
|
|
2003
3024
|
|
|
2004
3025
|
// src/core/verdict/egress-classify.ts
|
|
@@ -2219,7 +3240,7 @@ function verdictFingerprint(cwdRelative2, commandRedacted) {
|
|
|
2219
3240
|
|
|
2220
3241
|
// src/core/verdict/launcher-resolve.ts
|
|
2221
3242
|
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
|
|
2222
|
-
import
|
|
3243
|
+
import path14 from "node:path";
|
|
2223
3244
|
var MAX_RESOLVE_DEPTH = 8;
|
|
2224
3245
|
var PNPM_BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
|
|
2225
3246
|
"add",
|
|
@@ -2266,7 +3287,7 @@ var PNPM_EXEC_LIKE_HEADS = /* @__PURE__ */ new Set([
|
|
|
2266
3287
|
"node"
|
|
2267
3288
|
]);
|
|
2268
3289
|
function readPackageJson(dir) {
|
|
2269
|
-
const packagePath =
|
|
3290
|
+
const packagePath = path14.join(dir, "package.json");
|
|
2270
3291
|
if (!existsSync4(packagePath)) {
|
|
2271
3292
|
return null;
|
|
2272
3293
|
}
|
|
@@ -2277,17 +3298,17 @@ function readPackageJson(dir) {
|
|
|
2277
3298
|
}
|
|
2278
3299
|
}
|
|
2279
3300
|
function findPackageJson(startDir, stopDir) {
|
|
2280
|
-
let current =
|
|
2281
|
-
const stop =
|
|
3301
|
+
let current = path14.resolve(startDir);
|
|
3302
|
+
const stop = path14.resolve(stopDir);
|
|
2282
3303
|
while (true) {
|
|
2283
|
-
const packagePath =
|
|
3304
|
+
const packagePath = path14.join(current, "package.json");
|
|
2284
3305
|
if (existsSync4(packagePath)) {
|
|
2285
3306
|
return packagePath;
|
|
2286
3307
|
}
|
|
2287
|
-
if (current === stop || current ===
|
|
3308
|
+
if (current === stop || current === path14.dirname(current)) {
|
|
2288
3309
|
return existsSync4(packagePath) ? packagePath : null;
|
|
2289
3310
|
}
|
|
2290
|
-
const parent =
|
|
3311
|
+
const parent = path14.dirname(current);
|
|
2291
3312
|
if (!parent.startsWith(stop) && parent !== current) {
|
|
2292
3313
|
}
|
|
2293
3314
|
if (parent === current) {
|
|
@@ -2344,7 +3365,7 @@ function resolveNpmRecipe(cwd, repoRoot, scriptName, extraArgs) {
|
|
|
2344
3365
|
}
|
|
2345
3366
|
return { recipes: [], opaque: true, reason: "package_json_missing" };
|
|
2346
3367
|
}
|
|
2347
|
-
const pkg = readPackageJson(
|
|
3368
|
+
const pkg = readPackageJson(path14.dirname(packagePath));
|
|
2348
3369
|
const scripts = pkg?.scripts;
|
|
2349
3370
|
if (!scripts || typeof scripts !== "object") {
|
|
2350
3371
|
return { recipes: [], opaque: true, reason: "package_scripts_missing" };
|
|
@@ -2409,20 +3430,20 @@ function parseMakefileRecipes(makefilePath) {
|
|
|
2409
3430
|
function resolveMakeRecipe(cwd, repoRoot, target) {
|
|
2410
3431
|
const candidates = ["Makefile", "makefile", "GNUmakefile"];
|
|
2411
3432
|
let makefilePath = null;
|
|
2412
|
-
let searchDir =
|
|
2413
|
-
const stop =
|
|
3433
|
+
let searchDir = path14.resolve(cwd);
|
|
3434
|
+
const stop = path14.resolve(repoRoot);
|
|
2414
3435
|
while (true) {
|
|
2415
3436
|
for (const name of candidates) {
|
|
2416
|
-
const candidate =
|
|
3437
|
+
const candidate = path14.join(searchDir, name);
|
|
2417
3438
|
if (existsSync4(candidate)) {
|
|
2418
3439
|
makefilePath = candidate;
|
|
2419
3440
|
break;
|
|
2420
3441
|
}
|
|
2421
3442
|
}
|
|
2422
|
-
if (makefilePath || searchDir === stop || searchDir ===
|
|
3443
|
+
if (makefilePath || searchDir === stop || searchDir === path14.dirname(searchDir)) {
|
|
2423
3444
|
break;
|
|
2424
3445
|
}
|
|
2425
|
-
searchDir =
|
|
3446
|
+
searchDir = path14.dirname(searchDir);
|
|
2426
3447
|
}
|
|
2427
3448
|
if (!makefilePath) {
|
|
2428
3449
|
return { recipes: [], opaque: true, reason: "unknown_local_effect" };
|
|
@@ -2526,7 +3547,7 @@ function askFromCustomExternal(opacity) {
|
|
|
2526
3547
|
}
|
|
2527
3548
|
|
|
2528
3549
|
// src/core/verdict/parser.ts
|
|
2529
|
-
import
|
|
3550
|
+
import path15 from "node:path";
|
|
2530
3551
|
|
|
2531
3552
|
// src/core/shell-substitution.ts
|
|
2532
3553
|
function findCommandSubstitutions(command) {
|
|
@@ -2776,7 +3797,7 @@ var INTERPRETER_SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
2776
3797
|
".sh"
|
|
2777
3798
|
]);
|
|
2778
3799
|
function normalizeHead(token) {
|
|
2779
|
-
const base =
|
|
3800
|
+
const base = path15.basename(token);
|
|
2780
3801
|
if (base && base !== "." && base !== "..") {
|
|
2781
3802
|
return base;
|
|
2782
3803
|
}
|
|
@@ -2881,7 +3902,7 @@ function isBareInterpreter(tokens) {
|
|
|
2881
3902
|
return false;
|
|
2882
3903
|
}
|
|
2883
3904
|
const scriptArg = args.find((token) => !token.startsWith("-"));
|
|
2884
|
-
if (scriptArg && INTERPRETER_SCRIPT_EXTENSIONS.has(
|
|
3905
|
+
if (scriptArg && INTERPRETER_SCRIPT_EXTENSIONS.has(path15.extname(scriptArg))) {
|
|
2885
3906
|
return false;
|
|
2886
3907
|
}
|
|
2887
3908
|
if (scriptArg) {
|
|
@@ -3180,13 +4201,36 @@ function tier0ExternalMatch(key, head, tokens) {
|
|
|
3180
4201
|
}
|
|
3181
4202
|
return false;
|
|
3182
4203
|
}
|
|
4204
|
+
function isBelayJudgeSelfCommand(tokens) {
|
|
4205
|
+
return tokens[0] === "belay" && tokens[1] === "judge" && (tokens[2] === "use" || tokens[2] === "status" || tokens[2] === "list" || tokens[2] === "test" || tokens[2] === "consent");
|
|
4206
|
+
}
|
|
4207
|
+
function isBelayConfigSelfCommand(tokens) {
|
|
4208
|
+
if (tokens[0] !== "belay" || tokens[1] !== "config") {
|
|
4209
|
+
return false;
|
|
4210
|
+
}
|
|
4211
|
+
const sub = tokens[2];
|
|
4212
|
+
if (!sub) {
|
|
4213
|
+
return true;
|
|
4214
|
+
}
|
|
4215
|
+
if (sub === "list" || sub === "get" || sub === "judge") {
|
|
4216
|
+
return true;
|
|
4217
|
+
}
|
|
4218
|
+
if (sub === "credential") {
|
|
4219
|
+
return true;
|
|
4220
|
+
}
|
|
4221
|
+
if (sub === "set" || sub === "unset") {
|
|
4222
|
+
const pathKey = tokens[3] ?? "";
|
|
4223
|
+
return pathKey.startsWith("judge.");
|
|
4224
|
+
}
|
|
4225
|
+
return false;
|
|
4226
|
+
}
|
|
3183
4227
|
function isBelaySelfCommand(tokens) {
|
|
3184
4228
|
const head = tokens[0] ?? "";
|
|
3185
4229
|
const subcommand = tokens[1] ?? "";
|
|
3186
|
-
return head === "belay" && BELAY_SELF_COMMANDS.has(subcommand);
|
|
4230
|
+
return head === "belay" && (BELAY_SELF_COMMANDS.has(subcommand) || isBelayJudgeSelfCommand(tokens) || isBelayConfigSelfCommand(tokens));
|
|
3187
4231
|
}
|
|
3188
4232
|
function tier0HighStakesRm(tokens, context) {
|
|
3189
|
-
const head = tokens[0] ?? "";
|
|
4233
|
+
const head = (tokens[0] ?? "").split("/").pop() ?? "";
|
|
3190
4234
|
if (head !== "rm") {
|
|
3191
4235
|
return null;
|
|
3192
4236
|
}
|
|
@@ -3209,34 +4253,44 @@ function tier0HighStakesRm(tokens, context) {
|
|
|
3209
4253
|
signals: ["missing_trusted_cwd", ...analysis.signals]
|
|
3210
4254
|
});
|
|
3211
4255
|
}
|
|
3212
|
-
if (analysis.isHighStakes) {
|
|
3213
|
-
return askVerdict({
|
|
3214
|
-
location: analysis.location,
|
|
3215
|
-
opacity: "transparent",
|
|
3216
|
-
effect: "local_mutation",
|
|
3217
|
-
confidence: "deterministic",
|
|
3218
|
-
reason: "high_stakes_path",
|
|
3219
|
-
signals: ["high_stakes_path", ...analysis.signals]
|
|
3220
|
-
});
|
|
3221
|
-
}
|
|
3222
4256
|
for (const target of targets) {
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
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;
|
|
3238
4282
|
}
|
|
3239
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
|
+
}
|
|
3240
4294
|
}
|
|
3241
4295
|
return null;
|
|
3242
4296
|
}
|
|
@@ -3435,6 +4489,9 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3435
4489
|
effect = "local_mutation";
|
|
3436
4490
|
}
|
|
3437
4491
|
const pathArgs = extractPathArgs(peeled);
|
|
4492
|
+
if (extractRedirectTargets(peeled).length > 0 && effect === "read_only") {
|
|
4493
|
+
effect = "local_mutation";
|
|
4494
|
+
}
|
|
3438
4495
|
const pathAnalysis = analyzePathTargets({
|
|
3439
4496
|
targets: pathArgs,
|
|
3440
4497
|
cwd: context.cwd,
|
|
@@ -3465,15 +4522,46 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3465
4522
|
});
|
|
3466
4523
|
}
|
|
3467
4524
|
}
|
|
3468
|
-
if (
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
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
|
+
}
|
|
3477
4565
|
}
|
|
3478
4566
|
if (segment.head === "find" && isFindDangerous(peeled)) {
|
|
3479
4567
|
return askVerdict({
|
|
@@ -3485,15 +4573,14 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3485
4573
|
signals: ["find_dangerous_action"]
|
|
3486
4574
|
});
|
|
3487
4575
|
}
|
|
3488
|
-
if (pathAnalysis.location === "repo_outside" || pathAnalysis.location === "mixed") {
|
|
3489
|
-
|
|
3490
|
-
return askVerdict({
|
|
4576
|
+
if (effect === "read_only" && (pathAnalysis.location === "repo_outside" || pathAnalysis.location === "mixed")) {
|
|
4577
|
+
return allowVerdict({
|
|
3491
4578
|
location: pathAnalysis.location,
|
|
3492
|
-
opacity
|
|
3493
|
-
effect:
|
|
4579
|
+
opacity,
|
|
4580
|
+
effect: "read_only",
|
|
3494
4581
|
confidence: "deterministic",
|
|
3495
|
-
reason: "
|
|
3496
|
-
signals: ["
|
|
4582
|
+
reason: "read_only",
|
|
4583
|
+
signals: ["read_only", ...pathAnalysis.signals]
|
|
3497
4584
|
});
|
|
3498
4585
|
}
|
|
3499
4586
|
if (pathAnalysis.location === "unknown" && pathArgs.length > 0 && LOCAL_MUTATION_KEYS.has(segment.head)) {
|
|
@@ -3506,7 +4593,25 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3506
4593
|
signals: ["unknown_location_mutation"]
|
|
3507
4594
|
});
|
|
3508
4595
|
}
|
|
3509
|
-
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";
|
|
3510
4615
|
let tier1Trace;
|
|
3511
4616
|
if (needsTier1) {
|
|
3512
4617
|
const tier1Text = recursiveScript ?? command;
|
|
@@ -3518,9 +4623,9 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3518
4623
|
tier1Trace = context.judge.lastTrace;
|
|
3519
4624
|
if (tier1RequiresAsk(tier1)) {
|
|
3520
4625
|
return askVerdict({
|
|
3521
|
-
location: pathAnalysis.location === "unknown" ? "unknown" : "repo_local",
|
|
4626
|
+
location: pathAnalysis.location === "unknown" ? "unknown" : pathAnalysis.location === "repo_outside" || pathAnalysis.location === "mixed" ? pathAnalysis.location : "repo_local",
|
|
3522
4627
|
opacity,
|
|
3523
|
-
effect: tier1.
|
|
4628
|
+
effect: !tier1.local_recoverable ? "remote_mutation" : effect,
|
|
3524
4629
|
confidence: "llm",
|
|
3525
4630
|
reason: "tier1_catastrophic",
|
|
3526
4631
|
signals: ["tier1_catastrophic", tier1.reason],
|
|
@@ -3528,6 +4633,19 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3528
4633
|
});
|
|
3529
4634
|
}
|
|
3530
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
|
+
}
|
|
3531
4649
|
if (pathAnalysis.location === "repo_local" && (effect === "read_only" || effect === "local_mutation") && opacity !== "opaque") {
|
|
3532
4650
|
return withJudgeTrace(
|
|
3533
4651
|
allowVerdict({
|
|
@@ -3647,6 +4765,12 @@ async function verdict(command, context) {
|
|
|
3647
4765
|
}
|
|
3648
4766
|
|
|
3649
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
|
+
}
|
|
3650
4774
|
function buildVerdictContext(params) {
|
|
3651
4775
|
const protectedArtifactRoots2 = [
|
|
3652
4776
|
...params.options?.protectedArtifactRoots ?? [],
|
|
@@ -3655,7 +4779,7 @@ function buildVerdictContext(params) {
|
|
|
3655
4779
|
return {
|
|
3656
4780
|
cwd: params.cwd,
|
|
3657
4781
|
repoRoot: params.repoRoot,
|
|
3658
|
-
trustedCwd: params.
|
|
4782
|
+
trustedCwd: resolveClassifierTrustedCwd(params.cwd, params.options, params.trustedCwd),
|
|
3659
4783
|
sensitivePaths: params.options?.sensitivePaths ?? params.config.classifier.sensitivePaths,
|
|
3660
4784
|
protectedArtifactRoots: protectedArtifactRoots2.length > 0 ? [...new Set(protectedArtifactRoots2)] : void 0,
|
|
3661
4785
|
customAllowCommands: params.options?.customAllowCommands ?? params.config.overrides.allow,
|
|
@@ -3787,7 +4911,7 @@ function extractPatch(payload) {
|
|
|
3787
4911
|
}
|
|
3788
4912
|
return null;
|
|
3789
4913
|
}
|
|
3790
|
-
function
|
|
4914
|
+
function applyPatchTargets2(patch) {
|
|
3791
4915
|
const targets = [];
|
|
3792
4916
|
for (const line of patch.split("\n")) {
|
|
3793
4917
|
const match = line.match(/^\*\*\* (Add|Delete|Update) File: (.+)$/);
|
|
@@ -3805,6 +4929,70 @@ function applyPatchTargets(patch) {
|
|
|
3805
4929
|
function normalizedToolName(toolName) {
|
|
3806
4930
|
return toolName.trim().toLowerCase();
|
|
3807
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
|
+
}
|
|
3808
4996
|
async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
|
|
3809
4997
|
const toolName = String(payload.tool_name ?? "");
|
|
3810
4998
|
const toolKind = normalizedToolName(toolName);
|
|
@@ -3900,7 +5088,7 @@ async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
|
|
|
3900
5088
|
};
|
|
3901
5089
|
}
|
|
3902
5090
|
const signals = [];
|
|
3903
|
-
const resolvedPath =
|
|
5091
|
+
const resolvedPath = path16.isAbsolute(filePath) ? filePath : path16.resolve(cwd, filePath);
|
|
3904
5092
|
const hitsProtectedRoot = protectedRoots.some((root) => pathWithinRoot(root, resolvedPath));
|
|
3905
5093
|
if (hitsProtectedRoot) {
|
|
3906
5094
|
signals.push("control_plane_path");
|
|
@@ -3921,35 +5109,35 @@ async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
|
|
|
3921
5109
|
const relativePath = relativeWithinRepo(repoRoot, resolvedPath);
|
|
3922
5110
|
if (relativePath === null) {
|
|
3923
5111
|
signals.push("outside_repo_path");
|
|
3924
|
-
return {
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
};
|
|
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
|
+
});
|
|
3937
5125
|
}
|
|
3938
5126
|
if (matchesSensitivePath(relativePath, sensitivePaths)) {
|
|
3939
5127
|
signals.push("sensitive_path");
|
|
3940
|
-
return {
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
};
|
|
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
|
+
});
|
|
3953
5141
|
}
|
|
3954
5142
|
if (FILE_DELETE_TOOL_NAMES.has(toolKind)) {
|
|
3955
5143
|
signals.push("file_delete");
|
|
@@ -3984,7 +5172,7 @@ async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
|
|
|
3984
5172
|
}
|
|
3985
5173
|
if (APPLY_PATCH_TOOL_NAMES.has(toolKind)) {
|
|
3986
5174
|
const patch = extractPatch(payload);
|
|
3987
|
-
const targets = patch ?
|
|
5175
|
+
const targets = patch ? applyPatchTargets2(patch) : [];
|
|
3988
5176
|
if (targets.length === 0) {
|
|
3989
5177
|
if (options.unknownLocalEffect === "deny") {
|
|
3990
5178
|
return {
|
|
@@ -4174,10 +5362,56 @@ function normalizeGatedAction(params) {
|
|
|
4174
5362
|
agentAssessment
|
|
4175
5363
|
};
|
|
4176
5364
|
}
|
|
4177
|
-
function
|
|
4178
|
-
if (options.brokerFsScope
|
|
4179
|
-
|
|
4180
|
-
|
|
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") {
|
|
4181
5415
|
return {
|
|
4182
5416
|
...result,
|
|
4183
5417
|
verdict: "allow_flagged",
|
|
@@ -4195,6 +5429,38 @@ function applyShellPeripheralPolicy(command, action, result, options) {
|
|
|
4195
5429
|
}
|
|
4196
5430
|
return result;
|
|
4197
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
|
+
}
|
|
4198
5464
|
async function classifyGatedAction(action, config, extraOptions = {}) {
|
|
4199
5465
|
const options = { ...classifierOptionsFromConfig(config), ...extraOptions };
|
|
4200
5466
|
if (action.kind === "shell") {
|
|
@@ -4202,26 +5468,47 @@ async function classifyGatedAction(action, config, extraOptions = {}) {
|
|
|
4202
5468
|
if (!command) {
|
|
4203
5469
|
throw new GateNormalizationError("Shell gated action requires a command.");
|
|
4204
5470
|
}
|
|
4205
|
-
let
|
|
4206
|
-
|
|
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);
|
|
4207
5474
|
if (!action.agentAssessment) {
|
|
4208
|
-
return
|
|
5475
|
+
return result2;
|
|
4209
5476
|
}
|
|
4210
|
-
const
|
|
4211
|
-
if (!
|
|
4212
|
-
return { ...
|
|
5477
|
+
const merged2 = mergeAgentAssessment(result2.assessment, action.agentAssessment);
|
|
5478
|
+
if (!merged2.mismatch) {
|
|
5479
|
+
return { ...result2, assessment: merged2.assessment };
|
|
4213
5480
|
}
|
|
4214
5481
|
return {
|
|
4215
|
-
...
|
|
5482
|
+
...result2,
|
|
4216
5483
|
verdict: "deny_pending_approval",
|
|
4217
5484
|
reason: "agent_assessment_mismatch",
|
|
4218
|
-
assessment:
|
|
5485
|
+
assessment: merged2.assessment
|
|
4219
5486
|
};
|
|
4220
5487
|
}
|
|
4221
5488
|
if (action.kind === "subagent") {
|
|
4222
5489
|
return classifySubagent(action.payload ?? {}, action.repoRoot, options);
|
|
4223
5490
|
}
|
|
4224
|
-
|
|
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
|
+
};
|
|
4225
5512
|
}
|
|
4226
5513
|
async function classifyGatedActionAsync(action, config, extraOptions = {}) {
|
|
4227
5514
|
return classifyGatedAction(action, config, extraOptions);
|
|
@@ -4247,9 +5534,9 @@ function gateEnabledForAction(config, action) {
|
|
|
4247
5534
|
}
|
|
4248
5535
|
|
|
4249
5536
|
// src/core/transactional/diff-evaluator.ts
|
|
4250
|
-
import
|
|
5537
|
+
import path17 from "node:path";
|
|
4251
5538
|
function categorizeChange(change, ctx) {
|
|
4252
|
-
const absolutePath = canonicalPath(
|
|
5539
|
+
const absolutePath = canonicalPath(path17.join(ctx.repoRoot, change.relativePath));
|
|
4253
5540
|
if (!pathWithinRoot(ctx.repoRoot, absolutePath)) {
|
|
4254
5541
|
return "repo_outside";
|
|
4255
5542
|
}
|
|
@@ -4271,15 +5558,6 @@ function observedAssessment(evaluation) {
|
|
|
4271
5558
|
if (evaluation.deletedCount > 0) {
|
|
4272
5559
|
signals.push("observed_deletions");
|
|
4273
5560
|
}
|
|
4274
|
-
if (evaluation.categories.includes("repo_outside") || evaluation.categories.includes("control_plane") || evaluation.categories.includes("sensitive_path")) {
|
|
4275
|
-
return {
|
|
4276
|
-
reversibility: "irreversible",
|
|
4277
|
-
external: evaluation.categories.includes("repo_outside"),
|
|
4278
|
-
blastRadius: evaluation.categories.includes("control_plane") ? "agent-belay control plane" : evaluation.categories.includes("repo_outside") ? "outside the repository" : "sensitive path",
|
|
4279
|
-
confidence: 1,
|
|
4280
|
-
signals
|
|
4281
|
-
};
|
|
4282
|
-
}
|
|
4283
5561
|
if (evaluation.categories.includes("large_deletion")) {
|
|
4284
5562
|
return {
|
|
4285
5563
|
reversibility: "irreversible",
|
|
@@ -4307,7 +5585,7 @@ function evaluateTransactionalDiff(changes, ctx) {
|
|
|
4307
5585
|
categories.add("large_deletion");
|
|
4308
5586
|
}
|
|
4309
5587
|
const categoryList = [...categories];
|
|
4310
|
-
const dangerous = categoryList.includes("
|
|
5588
|
+
const dangerous = categoryList.includes("large_deletion");
|
|
4311
5589
|
const base = {
|
|
4312
5590
|
categories: categoryList,
|
|
4313
5591
|
changes,
|
|
@@ -4378,15 +5656,15 @@ var TRANSACTIONAL_APPROVAL_BYPASS_REASONS = /* @__PURE__ */ new Set([
|
|
|
4378
5656
|
]);
|
|
4379
5657
|
|
|
4380
5658
|
// src/core/transactional/git-worktree.ts
|
|
4381
|
-
import { spawn } from "node:child_process";
|
|
5659
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
4382
5660
|
import { randomUUID } from "node:crypto";
|
|
4383
5661
|
import { existsSync as existsSync5 } from "node:fs";
|
|
4384
|
-
import { copyFile, mkdir as
|
|
5662
|
+
import { copyFile, mkdir as mkdir3, mkdtemp, rm } from "node:fs/promises";
|
|
4385
5663
|
import os from "node:os";
|
|
4386
|
-
import
|
|
5664
|
+
import path18 from "node:path";
|
|
4387
5665
|
function execGit(repoRoot, args) {
|
|
4388
5666
|
return new Promise((resolve, reject) => {
|
|
4389
|
-
const child =
|
|
5667
|
+
const child = spawn2("git", ["-C", repoRoot, ...args], {
|
|
4390
5668
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4391
5669
|
});
|
|
4392
5670
|
const stdout = [];
|
|
@@ -4424,8 +5702,8 @@ async function isDirtyWorktree(repoRoot) {
|
|
|
4424
5702
|
}
|
|
4425
5703
|
}
|
|
4426
5704
|
async function createGitWorktreeSnapshot(repoRoot, stateDir) {
|
|
4427
|
-
const worktreePath =
|
|
4428
|
-
await
|
|
5705
|
+
const worktreePath = path18.join(stateDir, `tx-${randomUUID().replaceAll("-", "")}`);
|
|
5706
|
+
await mkdir3(stateDir, { recursive: true });
|
|
4429
5707
|
await execGit(repoRoot, ["worktree", "add", "--detach", worktreePath, "HEAD"]);
|
|
4430
5708
|
return {
|
|
4431
5709
|
worktreePath,
|
|
@@ -4444,18 +5722,18 @@ async function createGitWorktreeSnapshot(repoRoot, stateDir) {
|
|
|
4444
5722
|
}
|
|
4445
5723
|
function resolveWorktreeCwd(repoRoot, worktreePath, cwd) {
|
|
4446
5724
|
const resolvedCwd = canonicalPath(cwd);
|
|
4447
|
-
const relative =
|
|
4448
|
-
if (relative.startsWith("..") ||
|
|
5725
|
+
const relative = path18.relative(canonicalPath(repoRoot), resolvedCwd);
|
|
5726
|
+
if (relative.startsWith("..") || path18.isAbsolute(relative)) {
|
|
4449
5727
|
return worktreePath;
|
|
4450
5728
|
}
|
|
4451
5729
|
if (relative === "") {
|
|
4452
5730
|
return worktreePath;
|
|
4453
5731
|
}
|
|
4454
|
-
return
|
|
5732
|
+
return path18.join(worktreePath, relative);
|
|
4455
5733
|
}
|
|
4456
5734
|
function runShellCommand(command, cwd, timeoutMs) {
|
|
4457
5735
|
return new Promise((resolve) => {
|
|
4458
|
-
const child =
|
|
5736
|
+
const child = spawn2(command, {
|
|
4459
5737
|
cwd,
|
|
4460
5738
|
shell: true,
|
|
4461
5739
|
stdio: "ignore",
|
|
@@ -4521,7 +5799,7 @@ async function rollbackAppliedChanges(actions) {
|
|
|
4521
5799
|
for (const action of [...actions].reverse()) {
|
|
4522
5800
|
try {
|
|
4523
5801
|
if (action.type === "restore") {
|
|
4524
|
-
await
|
|
5802
|
+
await mkdir3(path18.dirname(action.target), { recursive: true });
|
|
4525
5803
|
await copyFile(action.backupPath, action.target);
|
|
4526
5804
|
} else {
|
|
4527
5805
|
await rm(action.target, { force: true });
|
|
@@ -4531,14 +5809,14 @@ async function rollbackAppliedChanges(actions) {
|
|
|
4531
5809
|
}
|
|
4532
5810
|
}
|
|
4533
5811
|
async function applyWorktreeChanges(worktreePath, repoRoot, changes) {
|
|
4534
|
-
const backupRoot = await mkdtemp(
|
|
5812
|
+
const backupRoot = await mkdtemp(path18.join(os.tmpdir(), "belay-tx-rollback-"));
|
|
4535
5813
|
const rollbackActions = [];
|
|
4536
5814
|
try {
|
|
4537
5815
|
for (const change of changes) {
|
|
4538
|
-
const target =
|
|
5816
|
+
const target = path18.join(repoRoot, change.relativePath);
|
|
4539
5817
|
if (existsSync5(target)) {
|
|
4540
|
-
const backupPath =
|
|
4541
|
-
await
|
|
5818
|
+
const backupPath = path18.join(backupRoot, change.relativePath);
|
|
5819
|
+
await mkdir3(path18.dirname(backupPath), { recursive: true });
|
|
4542
5820
|
await copyFile(target, backupPath);
|
|
4543
5821
|
rollbackActions.push({ type: "restore", target, backupPath });
|
|
4544
5822
|
} else if (change.kind !== "deleted") {
|
|
@@ -4548,8 +5826,8 @@ async function applyWorktreeChanges(worktreePath, repoRoot, changes) {
|
|
|
4548
5826
|
await rm(target, { force: true });
|
|
4549
5827
|
continue;
|
|
4550
5828
|
}
|
|
4551
|
-
const source =
|
|
4552
|
-
await
|
|
5829
|
+
const source = path18.join(worktreePath, change.relativePath);
|
|
5830
|
+
await mkdir3(path18.dirname(target), { recursive: true });
|
|
4553
5831
|
await copyFile(source, target);
|
|
4554
5832
|
}
|
|
4555
5833
|
} catch (error) {
|
|
@@ -4711,7 +5989,7 @@ async function notifyDeny(config, event) {
|
|
|
4711
5989
|
}
|
|
4712
5990
|
|
|
4713
5991
|
// src/adapters/layouts/protected-paths.ts
|
|
4714
|
-
import
|
|
5992
|
+
import path19 from "node:path";
|
|
4715
5993
|
function protectedArtifactRoots(layout, repoRoot, controlPlaneDir) {
|
|
4716
5994
|
const roots = [
|
|
4717
5995
|
layout.configPath(repoRoot),
|
|
@@ -4723,7 +6001,7 @@ function protectedArtifactRoots(layout, repoRoot, controlPlaneDir) {
|
|
|
4723
6001
|
if (controlPlaneDir) {
|
|
4724
6002
|
roots.push(controlPlaneDir);
|
|
4725
6003
|
}
|
|
4726
|
-
return roots.map((entry) =>
|
|
6004
|
+
return roots.map((entry) => path19.resolve(entry));
|
|
4727
6005
|
}
|
|
4728
6006
|
|
|
4729
6007
|
// src/adapters/shared/gate-runtime.ts
|
|
@@ -4733,7 +6011,7 @@ var EMPTY_APPROVALS = {
|
|
|
4733
6011
|
};
|
|
4734
6012
|
async function loadJsonFile(filePath, fallback) {
|
|
4735
6013
|
try {
|
|
4736
|
-
const raw = await
|
|
6014
|
+
const raw = await readFile3(filePath, "utf8");
|
|
4737
6015
|
return JSON.parse(raw);
|
|
4738
6016
|
} catch {
|
|
4739
6017
|
return fallback;
|
|
@@ -4745,14 +6023,14 @@ function createDefaultGateRuntimeDeps() {
|
|
|
4745
6023
|
return loadJsonFile(configPath, {});
|
|
4746
6024
|
},
|
|
4747
6025
|
async appendAudit(ctx, event) {
|
|
4748
|
-
const auditPath =
|
|
4749
|
-
await
|
|
6026
|
+
const auditPath = path20.join(ctx.repoRoot, ctx.config.audit.logPath);
|
|
6027
|
+
await mkdir4(path20.dirname(auditPath), { recursive: true });
|
|
4750
6028
|
const record = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), ...event };
|
|
4751
6029
|
if (!ctx.config.audit.includeAssessment) {
|
|
4752
6030
|
delete record.assessment;
|
|
4753
6031
|
}
|
|
4754
6032
|
const scrubbed = scrubValue(record, scrubOptionsFromConfig(ctx.config));
|
|
4755
|
-
await
|
|
6033
|
+
await writeFile3(auditPath, `${JSON.stringify(scrubbed)}
|
|
4756
6034
|
`, {
|
|
4757
6035
|
encoding: "utf8",
|
|
4758
6036
|
flag: "a"
|
|
@@ -4771,8 +6049,8 @@ function createDefaultGateRuntimeDeps() {
|
|
|
4771
6049
|
};
|
|
4772
6050
|
},
|
|
4773
6051
|
async writeApprovals(filePath, state) {
|
|
4774
|
-
await
|
|
4775
|
-
await
|
|
6052
|
+
await mkdir4(path20.dirname(filePath), { recursive: true });
|
|
6053
|
+
await writeFile3(filePath, `${JSON.stringify(compactApprovals(state), null, 2)}
|
|
4776
6054
|
`, "utf8");
|
|
4777
6055
|
}
|
|
4778
6056
|
};
|
|
@@ -4782,7 +6060,7 @@ async function resolveGateConfig(ctx, deps) {
|
|
|
4782
6060
|
let teamConfig = null;
|
|
4783
6061
|
const teamPath = teamConfigPath();
|
|
4784
6062
|
if (existsSync6(teamPath)) {
|
|
4785
|
-
teamConfig = JSON.parse(await
|
|
6063
|
+
teamConfig = JSON.parse(await readFile3(teamPath, "utf8"));
|
|
4786
6064
|
}
|
|
4787
6065
|
return resolveLayeredConfig({
|
|
4788
6066
|
repoConfig: loaded,
|
|
@@ -4927,7 +6205,7 @@ async function evaluateGatedAction(ctx, deps, params) {
|
|
|
4927
6205
|
command: params.command,
|
|
4928
6206
|
cwd: params.cwd,
|
|
4929
6207
|
repoRoot: ctx.repoRoot,
|
|
4930
|
-
stateDir:
|
|
6208
|
+
stateDir: path20.join(ctx.layout.repoLocalStateDir(ctx.repoRoot), "transactional"),
|
|
4931
6209
|
timeoutMs: transactional.timeoutMs,
|
|
4932
6210
|
predicted,
|
|
4933
6211
|
diffContext: {
|
|
@@ -5002,7 +6280,17 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
5002
6280
|
});
|
|
5003
6281
|
}
|
|
5004
6282
|
const brokerActive = isCapabilityBrokerDemotionActive(ctx.config);
|
|
5005
|
-
|
|
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
|
+
}
|
|
5006
6294
|
if (approved) {
|
|
5007
6295
|
await deps.appendAudit(ctx, {
|
|
5008
6296
|
...gateBase,
|
|
@@ -5180,18 +6468,18 @@ async function appendObservedAudit(ctx, deps, eventName, payload) {
|
|
|
5180
6468
|
|
|
5181
6469
|
// src/adapters/shared/repo-root.ts
|
|
5182
6470
|
import { existsSync as existsSync7 } from "node:fs";
|
|
5183
|
-
import
|
|
6471
|
+
import path21 from "node:path";
|
|
5184
6472
|
function findRepoRoot(startPath, layout) {
|
|
5185
|
-
let current =
|
|
6473
|
+
let current = path21.resolve(startPath);
|
|
5186
6474
|
while (true) {
|
|
5187
6475
|
for (const marker of layout.repoRootMarkers) {
|
|
5188
|
-
if (existsSync7(
|
|
6476
|
+
if (existsSync7(path21.join(current, marker))) {
|
|
5189
6477
|
return current;
|
|
5190
6478
|
}
|
|
5191
6479
|
}
|
|
5192
|
-
const parent =
|
|
6480
|
+
const parent = path21.dirname(current);
|
|
5193
6481
|
if (parent === current) {
|
|
5194
|
-
return
|
|
6482
|
+
return path21.resolve(startPath);
|
|
5195
6483
|
}
|
|
5196
6484
|
current = parent;
|
|
5197
6485
|
}
|