@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
package/dist/core/config.d.ts
CHANGED
|
@@ -76,13 +76,32 @@ export interface BelayRedactionConfig {
|
|
|
76
76
|
maskKeyValueSecrets: boolean;
|
|
77
77
|
maskHighEntropyStrings: boolean;
|
|
78
78
|
}
|
|
79
|
-
export type JudgeProvider = 'ollama' | 'openai-compatible';
|
|
79
|
+
export type JudgeProvider = 'ollama' | 'openai-compatible' | 'anthropic';
|
|
80
|
+
export type JudgeProviderId = 'ollama' | 'codex' | 'claude' | 'cursor';
|
|
81
|
+
/** Read-only legacy ids; preserved on load with warning until `belay config` migrates. */
|
|
82
|
+
export type DeprecatedJudgeProviderId = 'openrouter' | 'custom';
|
|
83
|
+
export type JudgeCredentialMode = 'project' | 'apiKey';
|
|
84
|
+
export type JudgeCredentialRef = `store:judge` | `env:${string}`;
|
|
85
|
+
export interface JudgeCredentialConfig {
|
|
86
|
+
mode: JudgeCredentialMode;
|
|
87
|
+
ref?: JudgeCredentialRef;
|
|
88
|
+
}
|
|
89
|
+
export interface JudgeCloudConsent {
|
|
90
|
+
accepted: boolean;
|
|
91
|
+
at: string;
|
|
92
|
+
providerId: JudgeProviderId;
|
|
93
|
+
endpoint: string;
|
|
94
|
+
by: string;
|
|
95
|
+
}
|
|
80
96
|
export interface BelayJudgeConfig {
|
|
81
97
|
provider: JudgeProvider;
|
|
98
|
+
providerId?: JudgeProviderId | DeprecatedJudgeProviderId;
|
|
82
99
|
model: string;
|
|
83
100
|
timeoutMs: number;
|
|
84
101
|
endpoint: string | null;
|
|
85
102
|
keepAlive: string | null;
|
|
103
|
+
cloudConsent?: JudgeCloudConsent;
|
|
104
|
+
credential?: JudgeCredentialConfig;
|
|
86
105
|
}
|
|
87
106
|
export declare const DEFAULT_JUDGE_LOCAL_OLLAMA: BelayJudgeConfig;
|
|
88
107
|
export declare const DEFAULT_JUDGE_OPENAI_COMPATIBLE_TEMPLATE: BelayJudgeConfig;
|
|
@@ -180,8 +199,9 @@ export declare function isConfigV1(value: unknown): value is BelayConfigV1;
|
|
|
180
199
|
export declare function isConfigV2(value: unknown): value is BelayConfigV2;
|
|
181
200
|
export declare function isConfigV3(value: unknown): value is BelayConfigV4;
|
|
182
201
|
export declare function isConfigV4(value: unknown): value is BelayConfigV4;
|
|
183
|
-
export declare function normalizeJudgeProvider(provider: string | undefined): 'ollama' | 'openai-compatible';
|
|
202
|
+
export declare function normalizeJudgeProvider(provider: string | undefined): 'ollama' | 'openai-compatible' | 'anthropic';
|
|
184
203
|
export declare function normalizeJudgeConfig(judge: BelayJudgeConfig): BelayJudgeConfig;
|
|
204
|
+
export declare function rejectTeamLayerJudgeSecrets(judge: Partial<BelayJudgeConfig> | undefined, source: 'team' | 'repo'): void;
|
|
185
205
|
export declare function migrateV3ToV4(v3: BelayConfigV4, raw?: RawConfigInput): BelayConfigV4;
|
|
186
206
|
type RawConfigInput = Partial<{
|
|
187
207
|
version: number;
|
package/dist/core/config.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { warnDeprecatedJudgeModelAuto } from './judge-model-policy.js';
|
|
3
|
+
import { getJudgeProviderSpec, inferProviderIdFromConfig, isRemovedProviderId, normalizeLegacyProviderId, warnRemovedProviderId, } from './verdict/judge-catalog.js';
|
|
2
4
|
export const DEFAULT_JUDGE_LOCAL_OLLAMA = {
|
|
3
5
|
provider: 'ollama',
|
|
6
|
+
providerId: 'ollama',
|
|
4
7
|
model: 'gemma4:e2b',
|
|
5
8
|
endpoint: 'http://localhost:11434',
|
|
6
9
|
timeoutMs: 25000,
|
|
@@ -8,7 +11,8 @@ export const DEFAULT_JUDGE_LOCAL_OLLAMA = {
|
|
|
8
11
|
};
|
|
9
12
|
export const DEFAULT_JUDGE_OPENAI_COMPATIBLE_TEMPLATE = {
|
|
10
13
|
provider: 'openai-compatible',
|
|
11
|
-
|
|
14
|
+
providerId: 'codex',
|
|
15
|
+
model: 'gpt-5.3-codex-high',
|
|
12
16
|
timeoutMs: 8000,
|
|
13
17
|
endpoint: null,
|
|
14
18
|
keepAlive: null,
|
|
@@ -209,44 +213,139 @@ export function isConfigV4(value) {
|
|
|
209
213
|
return typeof value === 'object' && value !== null && value.version === 4;
|
|
210
214
|
}
|
|
211
215
|
export function normalizeJudgeProvider(provider) {
|
|
216
|
+
if (provider === 'anthropic') {
|
|
217
|
+
return 'anthropic';
|
|
218
|
+
}
|
|
212
219
|
if (provider === 'openai-compatible' || provider === 'cursor') {
|
|
213
220
|
return 'openai-compatible';
|
|
214
221
|
}
|
|
215
222
|
return 'ollama';
|
|
216
223
|
}
|
|
224
|
+
function defaultJudgeTemplateForProvider(provider) {
|
|
225
|
+
if (provider === 'ollama') {
|
|
226
|
+
return DEFAULT_JUDGE_LOCAL_OLLAMA;
|
|
227
|
+
}
|
|
228
|
+
if (provider === 'anthropic') {
|
|
229
|
+
return {
|
|
230
|
+
provider: 'anthropic',
|
|
231
|
+
providerId: 'claude',
|
|
232
|
+
model: 'claude-sonnet-4-6',
|
|
233
|
+
timeoutMs: 8000,
|
|
234
|
+
endpoint: null,
|
|
235
|
+
keepAlive: null,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return DEFAULT_JUDGE_OPENAI_COMPATIBLE_TEMPLATE;
|
|
239
|
+
}
|
|
217
240
|
function synthesizeJudgeFromRaw(raw) {
|
|
218
241
|
const judge = raw.judge;
|
|
219
242
|
if (judge?.provider) {
|
|
220
|
-
const
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
243
|
+
const rawProvider = String(judge.provider);
|
|
244
|
+
const provider = normalizeJudgeProvider(rawProvider);
|
|
245
|
+
const base = defaultJudgeTemplateForProvider(provider);
|
|
246
|
+
const providerId = judge.providerId && normalizeLegacyProviderId(judge.providerId)
|
|
247
|
+
? normalizeLegacyProviderId(judge.providerId)
|
|
248
|
+
: rawProvider === 'cursor'
|
|
249
|
+
? 'cursor'
|
|
250
|
+
: undefined;
|
|
224
251
|
return normalizeJudgeConfig({
|
|
225
252
|
...base,
|
|
226
253
|
...judge,
|
|
227
254
|
provider,
|
|
255
|
+
...(providerId ? { providerId } : {}),
|
|
228
256
|
});
|
|
229
257
|
}
|
|
230
258
|
return { ...DEFAULT_JUDGE_LOCAL_OLLAMA };
|
|
231
259
|
}
|
|
232
260
|
export function normalizeJudgeConfig(judge) {
|
|
233
261
|
const provider = normalizeJudgeProvider(judge.provider);
|
|
234
|
-
const base = provider
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
262
|
+
const base = defaultJudgeTemplateForProvider(provider);
|
|
263
|
+
const rawProviderId = judge.providerId ? String(judge.providerId) : undefined;
|
|
264
|
+
if (rawProviderId && isRemovedProviderId(rawProviderId)) {
|
|
265
|
+
warnRemovedProviderId(rawProviderId);
|
|
266
|
+
let model = typeof judge.model === 'string' && judge.model.trim() ? judge.model.trim() : base.model;
|
|
267
|
+
if (model === 'auto') {
|
|
268
|
+
warnDeprecatedJudgeModelAuto();
|
|
269
|
+
model = base.model;
|
|
270
|
+
}
|
|
271
|
+
const timeoutMs = typeof judge.timeoutMs === 'number' && judge.timeoutMs > 0 ? judge.timeoutMs : base.timeoutMs;
|
|
272
|
+
const endpoint = typeof judge.endpoint === 'string' && judge.endpoint.trim() ? judge.endpoint.trim() : null;
|
|
273
|
+
return {
|
|
274
|
+
provider,
|
|
275
|
+
providerId: rawProviderId,
|
|
276
|
+
model,
|
|
277
|
+
timeoutMs,
|
|
278
|
+
endpoint,
|
|
279
|
+
keepAlive: provider === 'ollama' && typeof judge.keepAlive === 'string' && judge.keepAlive.trim()
|
|
280
|
+
? judge.keepAlive.trim()
|
|
281
|
+
: provider === 'ollama'
|
|
282
|
+
? DEFAULT_JUDGE_LOCAL_OLLAMA.keepAlive
|
|
283
|
+
: null,
|
|
284
|
+
...(judge.cloudConsent ? { cloudConsent: judge.cloudConsent } : {}),
|
|
285
|
+
...(judge.credential ? { credential: judge.credential } : {}),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
let providerId = rawProviderId && normalizeLegacyProviderId(rawProviderId)
|
|
289
|
+
? normalizeLegacyProviderId(rawProviderId)
|
|
290
|
+
: inferProviderIdFromConfig({ ...judge, provider });
|
|
291
|
+
const spec = getJudgeProviderSpec(providerId);
|
|
292
|
+
if (spec && spec.driver !== provider) {
|
|
293
|
+
providerId = inferProviderIdFromConfig({ ...judge, provider });
|
|
294
|
+
}
|
|
295
|
+
const catalogSpec = getJudgeProviderSpec(providerId);
|
|
296
|
+
let model = typeof judge.model === 'string' && judge.model.trim() ? judge.model.trim() : base.model;
|
|
297
|
+
if (model === 'auto' && catalogSpec?.defaultModel) {
|
|
298
|
+
warnDeprecatedJudgeModelAuto();
|
|
299
|
+
model = catalogSpec.defaultModel;
|
|
300
|
+
}
|
|
301
|
+
if (!model && catalogSpec?.defaultModel) {
|
|
302
|
+
model = catalogSpec.defaultModel;
|
|
303
|
+
}
|
|
238
304
|
const timeoutMs = typeof judge.timeoutMs === 'number' && judge.timeoutMs > 0 ? judge.timeoutMs : base.timeoutMs;
|
|
239
|
-
|
|
305
|
+
let endpoint = typeof judge.endpoint === 'string' && judge.endpoint.trim() ? judge.endpoint.trim() : null;
|
|
306
|
+
if (!endpoint && catalogSpec?.defaultEndpoint) {
|
|
307
|
+
endpoint = catalogSpec.defaultEndpoint;
|
|
308
|
+
}
|
|
309
|
+
const normalized = {
|
|
240
310
|
provider,
|
|
311
|
+
providerId,
|
|
241
312
|
model,
|
|
242
313
|
timeoutMs,
|
|
243
|
-
endpoint
|
|
314
|
+
endpoint,
|
|
244
315
|
keepAlive: provider === 'ollama' && typeof judge.keepAlive === 'string' && judge.keepAlive.trim()
|
|
245
316
|
? judge.keepAlive.trim()
|
|
246
317
|
: provider === 'ollama'
|
|
247
318
|
? DEFAULT_JUDGE_LOCAL_OLLAMA.keepAlive
|
|
248
319
|
: null,
|
|
249
320
|
};
|
|
321
|
+
if (judge.cloudConsent?.accepted) {
|
|
322
|
+
normalized.cloudConsent = {
|
|
323
|
+
accepted: true,
|
|
324
|
+
at: judge.cloudConsent.at,
|
|
325
|
+
providerId: judge.cloudConsent.providerId ?? providerId,
|
|
326
|
+
endpoint: judge.cloudConsent.endpoint,
|
|
327
|
+
by: judge.cloudConsent.by,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
if (judge.credential?.mode === 'project' || judge.credential?.mode === 'apiKey') {
|
|
331
|
+
normalized.credential = {
|
|
332
|
+
mode: judge.credential.mode,
|
|
333
|
+
...(judge.credential.ref ? { ref: judge.credential.ref } : {}),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
return normalized;
|
|
337
|
+
}
|
|
338
|
+
export function rejectTeamLayerJudgeSecrets(judge, source) {
|
|
339
|
+
if (source !== 'team' || !judge) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (judge.credential?.mode === 'apiKey') {
|
|
343
|
+
throw new Error('team config cannot set judge.credential.mode to apiKey.');
|
|
344
|
+
}
|
|
345
|
+
const raw = judge;
|
|
346
|
+
if (raw.credential && 'key' in raw.credential && raw.credential.key) {
|
|
347
|
+
throw new Error('team config cannot contain inline judge credential keys.');
|
|
348
|
+
}
|
|
250
349
|
}
|
|
251
350
|
export function migrateV3ToV4(v3, raw) {
|
|
252
351
|
return normalizeConfig({
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface JudgeCredentialStoreFile {
|
|
2
|
+
version: 1;
|
|
3
|
+
judge?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function credentialStorePath(stateDir: string): string;
|
|
6
|
+
export declare function writeJudgeCredentialStore(stateDir: string, apiKey: string): Promise<string>;
|
|
7
|
+
export declare function clearJudgeCredentialStore(stateDir: string): Promise<void>;
|
|
8
|
+
export declare function readJudgeCredentialStore(stateDir: string): Promise<string | null>;
|
|
9
|
+
export declare function judgeCredentialStoreExists(stateDir: string): Promise<boolean>;
|
|
10
|
+
export type CredentialRef = `store:judge` | `env:${string}`;
|
|
11
|
+
export declare function parseCredentialRef(ref: string): CredentialRef | null;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const STORE_FILENAME = 'credentials.json';
|
|
4
|
+
export function credentialStorePath(stateDir) {
|
|
5
|
+
return path.join(stateDir, STORE_FILENAME);
|
|
6
|
+
}
|
|
7
|
+
export async function writeJudgeCredentialStore(stateDir, apiKey) {
|
|
8
|
+
await mkdir(stateDir, { recursive: true, mode: 0o700 });
|
|
9
|
+
const filePath = credentialStorePath(stateDir);
|
|
10
|
+
const payload = {
|
|
11
|
+
version: 1,
|
|
12
|
+
judge: apiKey.trim(),
|
|
13
|
+
};
|
|
14
|
+
await writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, {
|
|
15
|
+
encoding: 'utf8',
|
|
16
|
+
mode: 0o600,
|
|
17
|
+
});
|
|
18
|
+
await chmod(filePath, 0o600);
|
|
19
|
+
return filePath;
|
|
20
|
+
}
|
|
21
|
+
export async function clearJudgeCredentialStore(stateDir) {
|
|
22
|
+
try {
|
|
23
|
+
await unlink(credentialStorePath(stateDir));
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.code !== 'ENOENT') {
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function readJudgeCredentialStore(stateDir) {
|
|
32
|
+
const filePath = credentialStorePath(stateDir);
|
|
33
|
+
try {
|
|
34
|
+
const raw = await readFile(filePath, 'utf8');
|
|
35
|
+
const parsed = JSON.parse(raw);
|
|
36
|
+
const key = parsed.judge?.trim();
|
|
37
|
+
return key || null;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function judgeCredentialStoreExists(stateDir) {
|
|
44
|
+
try {
|
|
45
|
+
await readFile(credentialStorePath(stateDir), 'utf8');
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function parseCredentialRef(ref) {
|
|
53
|
+
if (ref === 'store:judge') {
|
|
54
|
+
return ref;
|
|
55
|
+
}
|
|
56
|
+
if (ref.startsWith('env:') && ref.length > 4) {
|
|
57
|
+
return ref;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
package/dist/core/gate-engine.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { allPathsAllowlisted } from './capability/allowlist.js';
|
|
2
|
-
import { collectOutsideRepoPaths } from './capability/paths.js';
|
|
2
|
+
import { collectOutsideRepoPaths, collectOutsideRepoPathsFromToolPayload, } from './capability/paths.js';
|
|
3
3
|
import { classifySubagent } from './classify-subagent.js';
|
|
4
4
|
import { classifyToolUse } from './classify-tool.js';
|
|
5
5
|
import { classifierOptionsFromConfig } from './config.js';
|
|
@@ -92,17 +92,63 @@ export function normalizeGatedAction(params) {
|
|
|
92
92
|
agentAssessment,
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
|
-
function
|
|
96
|
-
if (options.brokerFsScope
|
|
97
|
-
result
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
95
|
+
function applySandboxFsScopeBoundary(outsideRepoPaths, result, options, hints = {}) {
|
|
96
|
+
if (!options.brokerFsScope) {
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
if (outsideRepoPaths.length === 0) {
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
if (options.fsScopeAllowlist && allPathsAllowlisted(outsideRepoPaths, options.fsScopeAllowlist)) {
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
if (result.verdict === 'deny_pending_approval') {
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
const redirect = hints.redirect === true ||
|
|
109
|
+
result.reason === 'read_only' ||
|
|
110
|
+
result.assessment.signals.includes('read_only') ||
|
|
111
|
+
result.reason === 'outside_repo_redirect';
|
|
112
|
+
return {
|
|
113
|
+
...result,
|
|
114
|
+
verdict: 'deny_pending_approval',
|
|
115
|
+
reason: redirect ? 'outside_repo_redirect' : 'outside_repo_mutation',
|
|
116
|
+
assessment: {
|
|
117
|
+
...result.assessment,
|
|
118
|
+
external: true,
|
|
119
|
+
reversibility: 'irreversible',
|
|
120
|
+
signals: [...result.assessment.signals, 'sandbox_boundary_expected'],
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function applyFsScopePeripheralPolicy(outsideRepoPaths, result, options) {
|
|
125
|
+
if (!options.brokerFsScope) {
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
if (outsideRepoPaths.length === 0) {
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
if (options.fsScopeAllowlist && allPathsAllowlisted(outsideRepoPaths, options.fsScopeAllowlist)) {
|
|
132
|
+
if (result.verdict === 'deny_pending_approval' &&
|
|
133
|
+
(result.reason === 'outside_repo_mutation' ||
|
|
134
|
+
result.reason === 'outside_repo_redirect' ||
|
|
135
|
+
result.reason === 'repo_outside_mutation' ||
|
|
136
|
+
result.axes?.location === 'repo_outside')) {
|
|
137
|
+
return {
|
|
138
|
+
...result,
|
|
139
|
+
verdict: 'allow_flagged',
|
|
140
|
+
reason: 'capability_fs_hint',
|
|
141
|
+
assessment: {
|
|
142
|
+
...result.assessment,
|
|
143
|
+
signals: [
|
|
144
|
+
...result.assessment.signals,
|
|
145
|
+
'capability_fs_hint',
|
|
146
|
+
'sandbox_boundary_expected',
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (result.verdict === 'allow' || result.verdict === 'allow_flagged') {
|
|
106
152
|
return {
|
|
107
153
|
...result,
|
|
108
154
|
verdict: 'allow_flagged',
|
|
@@ -120,6 +166,36 @@ function applyShellPeripheralPolicy(command, action, result, options) {
|
|
|
120
166
|
}
|
|
121
167
|
return result;
|
|
122
168
|
}
|
|
169
|
+
function applySandboxOutsideBoundary(command, action, result, options) {
|
|
170
|
+
const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot);
|
|
171
|
+
return applySandboxFsScopeBoundary(outsideRepoPaths, result, options, {
|
|
172
|
+
redirect: command.includes('>'),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function applyShellPeripheralPolicy(command, action, result, options) {
|
|
176
|
+
const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot);
|
|
177
|
+
return applyFsScopePeripheralPolicy(outsideRepoPaths, result, options);
|
|
178
|
+
}
|
|
179
|
+
function outsideRepoPathsForToolAction(action) {
|
|
180
|
+
const payload = action.payload ?? {};
|
|
181
|
+
const paths = new Set(collectOutsideRepoPathsFromToolPayload(payload, action.cwd, action.repoRoot));
|
|
182
|
+
const command = shellCommandFromPayload(payload);
|
|
183
|
+
if (command) {
|
|
184
|
+
for (const resolved of collectOutsideRepoPaths(command, action.cwd, action.repoRoot)) {
|
|
185
|
+
paths.add(resolved);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return [...paths];
|
|
189
|
+
}
|
|
190
|
+
function applyToolSandboxPolicies(action, result, options) {
|
|
191
|
+
const outsideRepoPaths = outsideRepoPathsForToolAction(action);
|
|
192
|
+
const command = shellCommandFromPayload(action.payload ?? {});
|
|
193
|
+
let next = applySandboxFsScopeBoundary(outsideRepoPaths, result, options, {
|
|
194
|
+
redirect: command.includes('>'),
|
|
195
|
+
});
|
|
196
|
+
next = applyFsScopePeripheralPolicy(outsideRepoPaths, next, options);
|
|
197
|
+
return next;
|
|
198
|
+
}
|
|
123
199
|
export async function classifyGatedAction(action, config, extraOptions = {}) {
|
|
124
200
|
const options = { ...classifierOptionsFromConfig(config), ...extraOptions };
|
|
125
201
|
if (action.kind === 'shell') {
|
|
@@ -128,6 +204,7 @@ export async function classifyGatedAction(action, config, extraOptions = {}) {
|
|
|
128
204
|
throw new GateNormalizationError('Shell gated action requires a command.');
|
|
129
205
|
}
|
|
130
206
|
let result = await classifyShell(command, action.cwd, action.repoRoot, config, options);
|
|
207
|
+
result = applySandboxOutsideBoundary(command, action, result, options);
|
|
131
208
|
result = applyShellPeripheralPolicy(command, action, result, options);
|
|
132
209
|
if (!action.agentAssessment) {
|
|
133
210
|
return result;
|
|
@@ -146,7 +223,21 @@ export async function classifyGatedAction(action, config, extraOptions = {}) {
|
|
|
146
223
|
if (action.kind === 'subagent') {
|
|
147
224
|
return classifySubagent(action.payload ?? {}, action.repoRoot, options);
|
|
148
225
|
}
|
|
149
|
-
|
|
226
|
+
let result = await classifyToolUse(action.payload ?? {}, action.repoRoot, action.cwd, config, options);
|
|
227
|
+
result = applyToolSandboxPolicies(action, result, options);
|
|
228
|
+
if (!action.agentAssessment) {
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
const merged = mergeAgentAssessment(result.assessment, action.agentAssessment);
|
|
232
|
+
if (!merged.mismatch) {
|
|
233
|
+
return { ...result, assessment: merged.assessment };
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
...result,
|
|
237
|
+
verdict: 'deny_pending_approval',
|
|
238
|
+
reason: 'agent_assessment_mismatch',
|
|
239
|
+
assessment: merged.assessment,
|
|
240
|
+
};
|
|
150
241
|
}
|
|
151
242
|
export async function classifyGatedActionAsync(action, config, extraOptions = {}) {
|
|
152
243
|
return classifyGatedAction(action, config, extraOptions);
|
package/dist/core/integrity.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ScopedPaths } from '../adapters/layouts/scope.js';
|
|
2
2
|
import type { AdapterLayout } from '../adapters/layouts/types.js';
|
|
3
|
+
import type { BelayConfigV4 } from './config.js';
|
|
3
4
|
export interface IntegrityManifest {
|
|
4
5
|
version: 1;
|
|
5
6
|
generatedAt: string;
|
|
@@ -13,3 +14,4 @@ export declare function verifyIntegrityManifest(repoRoot: string, layout: Adapte
|
|
|
13
14
|
ok: boolean;
|
|
14
15
|
mismatches: string[];
|
|
15
16
|
}>;
|
|
17
|
+
export declare function refreshIntegrityIfPinned(repoRoot: string, config: BelayConfigV4): Promise<void>;
|
package/dist/core/integrity.js
CHANGED
|
@@ -2,6 +2,9 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { getAdapterLayout } from '../adapters/layouts/index.js';
|
|
6
|
+
import { resolveScopedPaths } from '../adapters/layouts/scope.js';
|
|
7
|
+
import { resolveAdapterName } from '../config-io.js';
|
|
5
8
|
export async function sha256File(filePath) {
|
|
6
9
|
const content = await readFile(filePath);
|
|
7
10
|
return createHash('sha256').update(content).digest('hex');
|
|
@@ -66,3 +69,13 @@ export async function verifyIntegrityManifest(repoRoot, layout) {
|
|
|
66
69
|
}
|
|
67
70
|
return { ok: mismatches.length === 0, mismatches };
|
|
68
71
|
}
|
|
72
|
+
export async function refreshIntegrityIfPinned(repoRoot, config) {
|
|
73
|
+
if (config.controlPlane.integrity !== 'hash-pinned') {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const adapter = resolveAdapterName(config);
|
|
77
|
+
const layout = getAdapterLayout(adapter);
|
|
78
|
+
const installScope = config.installScope === 'global' ? 'global' : 'project';
|
|
79
|
+
const scoped = resolveScopedPaths(layout, installScope, repoRoot);
|
|
80
|
+
await writeIntegrityManifest(repoRoot, layout, runtimeIntegrityFiles(layout, scoped));
|
|
81
|
+
}
|
|
@@ -1,4 +1,23 @@
|
|
|
1
|
+
import type { BelayJudgeConfig } from './config.js';
|
|
2
|
+
import { belayStateDir } from './config.js';
|
|
3
|
+
import { type JudgeProviderSpec } from './verdict/judge-catalog.js';
|
|
4
|
+
export type JudgeCredentialSourceKind = 'env' | 'store' | 'host-session' | 'missing';
|
|
5
|
+
export interface ResolvedJudgeCredential {
|
|
6
|
+
key: string | null;
|
|
7
|
+
source: string | null;
|
|
8
|
+
sourceKind: JudgeCredentialSourceKind;
|
|
9
|
+
mode: 'project' | 'apiKey';
|
|
10
|
+
}
|
|
11
|
+
/** @deprecated Use resolveJudgeCredential */
|
|
1
12
|
export declare function resolveJudgeApiKey(env?: NodeJS.ProcessEnv): {
|
|
2
13
|
key: string | null;
|
|
3
14
|
source: 'BELAY_JUDGE_API_KEY' | 'OPENAI_API_KEY' | null;
|
|
4
15
|
};
|
|
16
|
+
export declare function resolveJudgeCredential(params: {
|
|
17
|
+
judge: BelayJudgeConfig;
|
|
18
|
+
catalogSpec?: JudgeProviderSpec;
|
|
19
|
+
repoRoot: string;
|
|
20
|
+
repoLocalStateDir: string;
|
|
21
|
+
config: Parameters<typeof belayStateDir>[0];
|
|
22
|
+
env?: NodeJS.ProcessEnv;
|
|
23
|
+
}): Promise<ResolvedJudgeCredential>;
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { belayStateDir } from './config.js';
|
|
2
|
+
import { parseCredentialRef, readJudgeCredentialStore } from './credential-store.js';
|
|
3
|
+
import { detectJudgeRuntimeCapabilities } from './judge-runtime-detection.js';
|
|
4
|
+
import { getJudgeProviderSpec, inferProviderIdFromConfig, normalizeLegacyProviderId, } from './verdict/judge-catalog.js';
|
|
5
|
+
/** @deprecated Use resolveJudgeCredential */
|
|
1
6
|
export function resolveJudgeApiKey(env = process.env) {
|
|
2
7
|
const belay = env.BELAY_JUDGE_API_KEY?.trim();
|
|
3
8
|
if (belay) {
|
|
@@ -9,3 +14,72 @@ export function resolveJudgeApiKey(env = process.env) {
|
|
|
9
14
|
}
|
|
10
15
|
return { key: null, source: null };
|
|
11
16
|
}
|
|
17
|
+
function resolveFromEnvChain(env, catalogSpec) {
|
|
18
|
+
const belay = env.BELAY_JUDGE_API_KEY?.trim();
|
|
19
|
+
if (belay) {
|
|
20
|
+
return { key: belay, source: 'BELAY_JUDGE_API_KEY' };
|
|
21
|
+
}
|
|
22
|
+
for (const name of catalogSpec?.apiKeyEnvVars ?? ['OPENAI_API_KEY', 'BELAY_JUDGE_API_KEY']) {
|
|
23
|
+
const value = env[name]?.trim();
|
|
24
|
+
if (value) {
|
|
25
|
+
return { key: value, source: name };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { key: null, source: null };
|
|
29
|
+
}
|
|
30
|
+
function sourceKindForProjectMode(fromEnv, judge, catalogSpec) {
|
|
31
|
+
if (fromEnv.key) {
|
|
32
|
+
return 'env';
|
|
33
|
+
}
|
|
34
|
+
const providerId = judge.providerId && normalizeLegacyProviderId(judge.providerId)
|
|
35
|
+
? normalizeLegacyProviderId(judge.providerId)
|
|
36
|
+
: inferProviderIdFromConfig(judge);
|
|
37
|
+
const spec = catalogSpec ?? getJudgeProviderSpec(providerId);
|
|
38
|
+
if (spec?.isCloud && detectJudgeRuntimeCapabilities(providerId).cliTransport) {
|
|
39
|
+
return 'host-session';
|
|
40
|
+
}
|
|
41
|
+
return 'missing';
|
|
42
|
+
}
|
|
43
|
+
function sourceKindForApiKeyMode(key, source) {
|
|
44
|
+
if (!key) {
|
|
45
|
+
return 'missing';
|
|
46
|
+
}
|
|
47
|
+
if (source?.startsWith('env:')) {
|
|
48
|
+
return 'env';
|
|
49
|
+
}
|
|
50
|
+
return 'store';
|
|
51
|
+
}
|
|
52
|
+
export async function resolveJudgeCredential(params) {
|
|
53
|
+
const env = params.env ?? process.env;
|
|
54
|
+
const credential = params.judge.credential ?? { mode: 'project' };
|
|
55
|
+
if (credential.mode === 'apiKey') {
|
|
56
|
+
const ref = credential.ref ? parseCredentialRef(credential.ref) : 'store:judge';
|
|
57
|
+
if (ref === 'store:judge') {
|
|
58
|
+
const stateDir = belayStateDir(params.config, params.repoLocalStateDir);
|
|
59
|
+
const key = await readJudgeCredentialStore(stateDir);
|
|
60
|
+
return {
|
|
61
|
+
key,
|
|
62
|
+
source: key ? `${stateDir}/credentials.json` : null,
|
|
63
|
+
sourceKind: sourceKindForApiKeyMode(key, key ? `${stateDir}/credentials.json` : null),
|
|
64
|
+
mode: 'apiKey',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (ref?.startsWith('env:')) {
|
|
68
|
+
const envName = ref.slice(4);
|
|
69
|
+
const value = env[envName]?.trim() ?? null;
|
|
70
|
+
return {
|
|
71
|
+
key: value,
|
|
72
|
+
source: value ? `env:${envName}` : null,
|
|
73
|
+
sourceKind: sourceKindForApiKeyMode(value, value ? `env:${envName}` : null),
|
|
74
|
+
mode: 'apiKey',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return { key: null, source: null, sourceKind: 'missing', mode: 'apiKey' };
|
|
78
|
+
}
|
|
79
|
+
const fromEnv = resolveFromEnvChain(env, params.catalogSpec);
|
|
80
|
+
return {
|
|
81
|
+
...fromEnv,
|
|
82
|
+
sourceKind: sourceKindForProjectMode(fromEnv, params.judge, params.catalogSpec),
|
|
83
|
+
mode: 'project',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { JUDGE_CLOUD_CONSENT_REASON } from './capability/reasons.js';
|
|
2
|
+
import type { BelayConfigV4, JudgeProviderId } from './config.js';
|
|
3
|
+
export { JUDGE_CLOUD_CONSENT_REASON };
|
|
4
|
+
export declare function judgeCloudConsentFingerprint(providerId: JudgeProviderId, endpoint: string): string;
|
|
5
|
+
export declare function ensurePendingJudgeCloudConsentApproval(params: {
|
|
6
|
+
repoRoot: string;
|
|
7
|
+
config: BelayConfigV4;
|
|
8
|
+
providerId: JudgeProviderId;
|
|
9
|
+
endpoint: string;
|
|
10
|
+
}): Promise<{
|
|
11
|
+
approvalId: string;
|
|
12
|
+
created: boolean;
|
|
13
|
+
}>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { loadApprovalState, saveApprovalState } from '../config-io.js';
|
|
3
|
+
import { compactApprovals, createApprovalRecord } from './approval.js';
|
|
4
|
+
import { JUDGE_CLOUD_CONSENT_REASON } from './capability/reasons.js';
|
|
5
|
+
export { JUDGE_CLOUD_CONSENT_REASON };
|
|
6
|
+
export function judgeCloudConsentFingerprint(providerId, endpoint) {
|
|
7
|
+
return `judge_cloud_consent:${providerId}:${endpoint.trim()}`;
|
|
8
|
+
}
|
|
9
|
+
export async function ensurePendingJudgeCloudConsentApproval(params) {
|
|
10
|
+
const { repoRoot, config, providerId, endpoint } = params;
|
|
11
|
+
const normalizedEndpoint = endpoint.trim();
|
|
12
|
+
const fingerprint = judgeCloudConsentFingerprint(providerId, normalizedEndpoint);
|
|
13
|
+
const pending = await loadApprovalState(repoRoot, 'pending-approvals.json', config);
|
|
14
|
+
const compacted = compactApprovals(pending);
|
|
15
|
+
const existing = compacted.approvals.find((approval) => approval.kind === 'capability' &&
|
|
16
|
+
approval.reason === JUDGE_CLOUD_CONSENT_REASON &&
|
|
17
|
+
approval.fingerprint === fingerprint &&
|
|
18
|
+
approval.repoRoot === repoRoot);
|
|
19
|
+
if (existing) {
|
|
20
|
+
await saveApprovalState(repoRoot, 'pending-approvals.json', compacted, config);
|
|
21
|
+
return { approvalId: existing.approvalId, created: false };
|
|
22
|
+
}
|
|
23
|
+
const approvalId = `belay_${randomUUID().replaceAll('-', '').slice(0, 12)}`;
|
|
24
|
+
const approval = createApprovalRecord({
|
|
25
|
+
kind: 'capability',
|
|
26
|
+
fingerprint,
|
|
27
|
+
repoRoot,
|
|
28
|
+
reason: JUDGE_CLOUD_CONSENT_REASON,
|
|
29
|
+
summary: `Cloud judge egress to ${providerId} (${normalizedEndpoint})`,
|
|
30
|
+
approvalTtlMinutes: config.approvalTtlMinutes,
|
|
31
|
+
approvalId,
|
|
32
|
+
input: `belay judge use ${providerId} --endpoint ${normalizedEndpoint}`,
|
|
33
|
+
inputKind: 'shell',
|
|
34
|
+
});
|
|
35
|
+
compacted.approvals.push(approval);
|
|
36
|
+
await saveApprovalState(repoRoot, 'pending-approvals.json', compacted, config);
|
|
37
|
+
return { approvalId, created: true };
|
|
38
|
+
}
|