@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,90 +1,79 @@
|
|
|
1
|
+
import { repoLocalStateDirFor } from '../config-io.js';
|
|
1
2
|
import { normalizeJudgeProvider, scrubOptionsFromConfig } from './config.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
3
|
+
import { resolveJudgeCredential } from './judge-api-key.js';
|
|
4
|
+
import { hasValidCloudConsent } from './judge-config.js';
|
|
5
|
+
import { discoverJudgeModels, modelPresenceFromDiscovery, } from './judge-model-discovery.js';
|
|
6
|
+
import { detectJudgeRuntimeCapabilities, resolveJudgeTransport } from './judge-runtime-detection.js';
|
|
4
7
|
import { createOllamaJudge, createOpenAiCompatibleJudge } from './verdict/judge.js';
|
|
5
|
-
import {
|
|
6
|
-
|
|
8
|
+
import { getJudgeProviderCapabilities, getJudgeProviderSpec, isRemovedProviderId, normalizeLegacyProviderId, } from './verdict/judge-catalog.js';
|
|
9
|
+
import { createJudgeFromConfig, resolveJudgeModel } from './verdict/judge-factory.js';
|
|
10
|
+
export async function diagnoseJudge(config, repoRoot = process.cwd(), options = {}) {
|
|
7
11
|
const issues = [];
|
|
8
12
|
const warnings = [];
|
|
9
13
|
const notes = [];
|
|
10
14
|
const judge = config.judge;
|
|
11
15
|
const provider = normalizeJudgeProvider(judge.provider);
|
|
12
|
-
|
|
16
|
+
const rawProviderId = judge.providerId ? String(judge.providerId) : undefined;
|
|
17
|
+
if (rawProviderId && isRemovedProviderId(rawProviderId)) {
|
|
18
|
+
notes.push(`Judge providerId: ${rawProviderId}`);
|
|
19
|
+
notes.push(`Judge driver: ${provider}`);
|
|
20
|
+
notes.push(`Judge model requested: ${judge.model}`);
|
|
21
|
+
issues.push(`judge.providerId "${rawProviderId}" was removed; run belay config set judge.providerId <ollama|codex|claude|cursor> to migrate.`);
|
|
22
|
+
return { issues, warnings, notes };
|
|
23
|
+
}
|
|
24
|
+
const providerId = judge.providerId && normalizeLegacyProviderId(judge.providerId)
|
|
25
|
+
? normalizeLegacyProviderId(judge.providerId)
|
|
26
|
+
: provider === 'ollama'
|
|
27
|
+
? 'ollama'
|
|
28
|
+
: provider === 'anthropic'
|
|
29
|
+
? 'claude'
|
|
30
|
+
: 'codex';
|
|
31
|
+
const catalogSpec = getJudgeProviderSpec(providerId);
|
|
32
|
+
const capabilities = getJudgeProviderCapabilities(providerId);
|
|
33
|
+
const transport = resolveJudgeTransport(judge);
|
|
34
|
+
const runtime = detectJudgeRuntimeCapabilities(providerId);
|
|
35
|
+
notes.push(`Judge providerId: ${providerId}`);
|
|
36
|
+
notes.push(`Judge driver: ${provider}`);
|
|
13
37
|
notes.push(`Judge model requested: ${judge.model}`);
|
|
38
|
+
notes.push(`Judge transport: ${transport}`);
|
|
14
39
|
if (config.policy.modelAssist.enabled) {
|
|
15
40
|
warnings.push('policy.modelAssist is enabled but is not wired to v2 Tier1. Use top-level judge instead.');
|
|
16
41
|
}
|
|
17
|
-
if (
|
|
42
|
+
if (capabilities?.requiresConsent && !hasValidCloudConsent(judge) && transport === 'http') {
|
|
43
|
+
issues.push('Cloud judge consent is not recorded. Tier1 cloud judge will fail closed until consent is granted.');
|
|
44
|
+
}
|
|
45
|
+
else if (judge.cloudConsent?.accepted) {
|
|
46
|
+
notes.push(`Cloud consent: accepted ${judge.cloudConsent.at} by ${judge.cloudConsent.by}`);
|
|
47
|
+
}
|
|
48
|
+
if (providerId !== 'ollama') {
|
|
18
49
|
warnings.push('Cloud judge egress is enabled. Commands are redacted (R23) before send, but path structure and intent may still leave the repo.');
|
|
19
|
-
try {
|
|
20
|
-
assertJudgeEndpoint(judge);
|
|
21
|
-
notes.push(`OpenAI-compatible endpoint: ${judge.endpoint}`);
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
issues.push('openai-compatible judge requires judge.endpoint. No default cloud base URL is applied.');
|
|
25
|
-
return { issues, warnings, notes };
|
|
26
|
-
}
|
|
27
|
-
const keyInfo = resolveJudgeApiKey();
|
|
28
|
-
if (!keyInfo.key) {
|
|
29
|
-
issues.push('BELAY_JUDGE_API_KEY / OPENAI_API_KEY is not set. Tier1 cloud judge will fail closed to ask.');
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
notes.push(`API key source: ${keyInfo.source}`);
|
|
33
|
-
}
|
|
34
|
-
const pinnedModels = await loadPinnedJudgeModels();
|
|
35
|
-
const resolved = resolveCloudModel(judge.model, pinnedModels['openai-compatible']);
|
|
36
|
-
notes.push(`Resolved model: ${resolved.resolved}`);
|
|
37
|
-
if (keyInfo.key && judge.endpoint?.trim()) {
|
|
38
|
-
const traced = createOpenAiCompatibleJudge({
|
|
39
|
-
endpoint: judge.endpoint.trim(),
|
|
40
|
-
modelRequested: judge.model,
|
|
41
|
-
modelResolved: resolved.resolved,
|
|
42
|
-
timeoutMs: Math.min(judge.timeoutMs, 5000),
|
|
43
|
-
apiKey: keyInfo.key,
|
|
44
|
-
sensitivePaths: config.classifier.sensitivePaths,
|
|
45
|
-
scrubOptions: scrubOptionsFromConfig(config),
|
|
46
|
-
fetchImpl: async () => new Response(JSON.stringify({
|
|
47
|
-
choices: [
|
|
48
|
-
{
|
|
49
|
-
message: {
|
|
50
|
-
content: JSON.stringify({
|
|
51
|
-
external_change: false,
|
|
52
|
-
destroys_outside_repo: false,
|
|
53
|
-
destroys_history_or_secrets: false,
|
|
54
|
-
reason: 'doctor_dry_run',
|
|
55
|
-
}),
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
],
|
|
59
|
-
}), { status: 200 }),
|
|
60
|
-
});
|
|
61
|
-
const dryRun = await traced.evaluate({
|
|
62
|
-
text: 'git status',
|
|
63
|
-
context: { cwd: process.cwd(), repoRoot: process.cwd() },
|
|
64
|
-
});
|
|
65
|
-
if (dryRun.reason.startsWith('openai_compatible_') ||
|
|
66
|
-
dryRun.reason === 'outbound_scrub_failed') {
|
|
67
|
-
issues.push(`OpenAI-compatible judge dry-run failed: ${dryRun.reason}`);
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
notes.push('OpenAI-compatible judge dry-run succeeded.');
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return { issues, warnings, notes };
|
|
74
50
|
}
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
51
|
+
const repoLocalDir = repoLocalStateDirFor(repoRoot, config);
|
|
52
|
+
const keyInfo = await resolveJudgeCredential({
|
|
53
|
+
judge,
|
|
54
|
+
catalogSpec: catalogSpec ?? undefined,
|
|
55
|
+
repoRoot,
|
|
56
|
+
repoLocalStateDir: repoLocalDir,
|
|
57
|
+
config,
|
|
58
|
+
});
|
|
59
|
+
notes.push(`Credential: ${keyInfo.mode} (${keyInfo.sourceKind})`);
|
|
60
|
+
const resolved = resolveJudgeModel(judge);
|
|
61
|
+
notes.push(`Resolved model: ${resolved.resolved}`);
|
|
62
|
+
const endpoint = judge.endpoint ?? (providerId === 'ollama' ? 'http://127.0.0.1:11434' : null);
|
|
63
|
+
const discovery = await discoverJudgeModels({
|
|
64
|
+
providerId,
|
|
65
|
+
model: judge.model,
|
|
66
|
+
endpoint,
|
|
67
|
+
}, options.discoveryDeps);
|
|
68
|
+
const modelCheck = modelPresenceFromDiscovery(discovery, judge.model);
|
|
69
|
+
notes.push(`Model check: ${modelCheck.status} (source: ${modelCheck.source})`);
|
|
70
|
+
if (providerId === 'ollama') {
|
|
71
|
+
notes.push(`Ollama endpoint: ${endpoint}`);
|
|
72
|
+
if (discovery.modelIds.length === 0) {
|
|
73
|
+
issues.push(`Ollama endpoint unreachable or returned no models. Tier1 will fail closed.`);
|
|
83
74
|
}
|
|
84
75
|
else {
|
|
85
|
-
const
|
|
86
|
-
const names = (tags.models ?? []).map((entry) => entry.name ?? '');
|
|
87
|
-
const hasModel = names.some((name) => name === judge.model || name.startsWith(`${judge.model}:`));
|
|
76
|
+
const hasModel = modelCheck.status === 'found';
|
|
88
77
|
if (!hasModel) {
|
|
89
78
|
issues.push(`Ollama model "${judge.model}" is not present. Pull it before enforce mode.`);
|
|
90
79
|
}
|
|
@@ -92,33 +81,95 @@ export async function diagnoseJudge(config) {
|
|
|
92
81
|
notes.push(`Ollama model "${judge.model}" is available.`);
|
|
93
82
|
}
|
|
94
83
|
}
|
|
84
|
+
const warm = createOllamaJudge({
|
|
85
|
+
model: judge.model,
|
|
86
|
+
baseUrl: endpoint ?? 'http://127.0.0.1:11434',
|
|
87
|
+
timeoutMs: Math.min(judge.timeoutMs, 5000),
|
|
88
|
+
fetchImpl: async () => new Response(JSON.stringify({
|
|
89
|
+
response: JSON.stringify({
|
|
90
|
+
local_recoverable: true,
|
|
91
|
+
destroys_outside_repo: false,
|
|
92
|
+
destroys_history_or_secrets: false,
|
|
93
|
+
reason: 'doctor_warm',
|
|
94
|
+
}),
|
|
95
|
+
}), { status: 200 }),
|
|
96
|
+
});
|
|
97
|
+
const warmResult = await warm.evaluate({
|
|
98
|
+
text: 'git status',
|
|
99
|
+
context: { cwd: process.cwd(), repoRoot: process.cwd() },
|
|
100
|
+
});
|
|
101
|
+
if (warmResult.reason === 'ollama_unavailable' || warmResult.reason === 'ollama_parse_error') {
|
|
102
|
+
issues.push(`Ollama warm call failed: ${warmResult.reason}`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
notes.push('Ollama warm call succeeded.');
|
|
106
|
+
}
|
|
107
|
+
return { issues, warnings, notes, modelCheck };
|
|
95
108
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
109
|
+
if (transport === 'unavailable') {
|
|
110
|
+
issues.push('No judge transport is available (configure endpoint or install native CLI). Tier1 will fail closed to ask.');
|
|
111
|
+
return { issues, warnings, notes, modelCheck };
|
|
99
112
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (warmResult.reason === 'ollama_unavailable' || warmResult.reason === 'ollama_parse_error') {
|
|
118
|
-
issues.push(`Ollama warm call failed: ${warmResult.reason}`);
|
|
113
|
+
if (transport.endsWith('-cli')) {
|
|
114
|
+
if (!runtime.cliTransport) {
|
|
115
|
+
issues.push(`Native CLI transport (${transport}) is not available. Tier1 judge will fail closed to ask.`);
|
|
116
|
+
}
|
|
117
|
+
else if (!keyInfo.key && keyInfo.sourceKind !== 'host-session') {
|
|
118
|
+
issues.push('Judge API key is not set for the configured credential mode. Tier1 cloud judge will fail closed to ask.');
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
notes.push(`Native CLI transport available: ${transport}`);
|
|
122
|
+
}
|
|
123
|
+
return { issues, warnings, notes, modelCheck };
|
|
124
|
+
}
|
|
125
|
+
if (judge.endpoint?.trim()) {
|
|
126
|
+
notes.push(`HTTP endpoint: ${judge.endpoint}`);
|
|
127
|
+
}
|
|
128
|
+
if (!keyInfo.key) {
|
|
129
|
+
issues.push('Judge API key is not set for the configured credential mode. Tier1 cloud judge will fail closed to ask.');
|
|
119
130
|
}
|
|
120
131
|
else {
|
|
121
|
-
notes.push(
|
|
132
|
+
notes.push(`Credential source: ${keyInfo.source ?? keyInfo.sourceKind}`);
|
|
133
|
+
}
|
|
134
|
+
if (keyInfo.key && judge.endpoint?.trim() && hasValidCloudConsent(judge)) {
|
|
135
|
+
const traced = createOpenAiCompatibleJudge({
|
|
136
|
+
endpoint: judge.endpoint.trim(),
|
|
137
|
+
modelRequested: judge.model,
|
|
138
|
+
modelResolved: resolved.resolved,
|
|
139
|
+
timeoutMs: Math.min(judge.timeoutMs, 5000),
|
|
140
|
+
apiKey: keyInfo.key,
|
|
141
|
+
sensitivePaths: config.classifier.sensitivePaths,
|
|
142
|
+
scrubOptions: scrubOptionsFromConfig(config),
|
|
143
|
+
fetchImpl: async () => new Response(JSON.stringify({
|
|
144
|
+
choices: [
|
|
145
|
+
{
|
|
146
|
+
message: {
|
|
147
|
+
content: JSON.stringify({
|
|
148
|
+
local_recoverable: true,
|
|
149
|
+
destroys_outside_repo: false,
|
|
150
|
+
destroys_history_or_secrets: false,
|
|
151
|
+
reason: 'doctor_dry_run',
|
|
152
|
+
}),
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
}), { status: 200 }),
|
|
157
|
+
});
|
|
158
|
+
const dryRun = await traced.evaluate({
|
|
159
|
+
text: 'git status',
|
|
160
|
+
context: { cwd: process.cwd(), repoRoot: process.cwd() },
|
|
161
|
+
});
|
|
162
|
+
if (dryRun.reason.startsWith('openai_compatible_') ||
|
|
163
|
+
dryRun.reason === 'outbound_scrub_failed') {
|
|
164
|
+
issues.push(`HTTP judge dry-run failed: ${dryRun.reason}`);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
notes.push('HTTP judge dry-run succeeded.');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const factoryJudge = createJudgeFromConfig(config, { repoRoot });
|
|
171
|
+
if (factoryJudge.lastTrace?.transport) {
|
|
172
|
+
notes.push(`Factory transport: ${factoryJudge.lastTrace.transport}`);
|
|
122
173
|
}
|
|
123
|
-
return { issues, warnings, notes };
|
|
174
|
+
return { issues, warnings, notes, modelCheck };
|
|
124
175
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface DiscoverJudgeModelsInput {
|
|
2
|
+
providerId: string;
|
|
3
|
+
model: string;
|
|
4
|
+
endpoint: string | null;
|
|
5
|
+
}
|
|
6
|
+
export interface DiscoverJudgeModelsResult {
|
|
7
|
+
source: string;
|
|
8
|
+
modelIds: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface CheckJudgeModelPresenceResult {
|
|
11
|
+
status: 'found' | 'missing' | 'unverified';
|
|
12
|
+
source: string;
|
|
13
|
+
}
|
|
14
|
+
export type JudgeModelDiscoveryRunCommand = (command: string, args: string[], timeoutMs: number) => Promise<string>;
|
|
15
|
+
export interface JudgeModelDiscoveryDeps {
|
|
16
|
+
runCommand?: JudgeModelDiscoveryRunCommand;
|
|
17
|
+
fetch?: typeof fetch;
|
|
18
|
+
allowCliDiscovery?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function parseLineModelIds(raw: string): string[];
|
|
21
|
+
export declare function parseJsonModelIds(raw: string): string[];
|
|
22
|
+
export declare function discoverJudgeModels(input: DiscoverJudgeModelsInput, deps?: JudgeModelDiscoveryDeps): Promise<DiscoverJudgeModelsResult>;
|
|
23
|
+
export declare function checkJudgeModelPresence(input: DiscoverJudgeModelsInput, deps?: JudgeModelDiscoveryDeps): Promise<CheckJudgeModelPresenceResult>;
|
|
24
|
+
export declare function modelPresenceFromDiscovery(discovery: DiscoverJudgeModelsResult, model: string): CheckJudgeModelPresenceResult;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { getJudgeProviderSpec, inferProviderIdFromConfig, normalizeLegacyProviderId, } from './verdict/judge-catalog.js';
|
|
3
|
+
const MODEL_DISCOVERY_SOURCES = {
|
|
4
|
+
ollama: 'ollama-tags',
|
|
5
|
+
codex: 'codex-cli',
|
|
6
|
+
claude: 'anthropic-models',
|
|
7
|
+
cursor: 'cursor-agent',
|
|
8
|
+
};
|
|
9
|
+
function providerIdFromInput(providerId) {
|
|
10
|
+
const normalized = normalizeLegacyProviderId(providerId);
|
|
11
|
+
if (normalized) {
|
|
12
|
+
return normalized;
|
|
13
|
+
}
|
|
14
|
+
return inferProviderIdFromConfig({ providerId: providerId });
|
|
15
|
+
}
|
|
16
|
+
function defaultAllowCliDiscovery() {
|
|
17
|
+
if (process.env.BELAY_JUDGE_DISABLE_CLI_TRANSPORT === '1') {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return !process.env.VITEST && !process.env.VITEST_WORKER_ID;
|
|
21
|
+
}
|
|
22
|
+
function resolveDeps(deps) {
|
|
23
|
+
return {
|
|
24
|
+
runCommand: deps?.runCommand ?? runCommandCapture,
|
|
25
|
+
fetchImpl: deps?.fetch ?? fetch,
|
|
26
|
+
allowCliDiscovery: deps?.allowCliDiscovery ?? defaultAllowCliDiscovery(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async function runCommandCapture(command, args, timeoutMs) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
32
|
+
let stdout = '';
|
|
33
|
+
let stderr = '';
|
|
34
|
+
const timer = setTimeout(() => {
|
|
35
|
+
child.kill('SIGTERM');
|
|
36
|
+
reject(new Error(`${command} timed out`));
|
|
37
|
+
}, timeoutMs);
|
|
38
|
+
child.stdout.on('data', (chunk) => {
|
|
39
|
+
stdout += String(chunk);
|
|
40
|
+
});
|
|
41
|
+
child.stderr.on('data', (chunk) => {
|
|
42
|
+
stderr += String(chunk);
|
|
43
|
+
});
|
|
44
|
+
child.on('error', (error) => {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
reject(error);
|
|
47
|
+
});
|
|
48
|
+
child.on('close', (code) => {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
if (code === 0 && stdout.trim()) {
|
|
51
|
+
resolve(stdout.trim());
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
reject(new Error(stderr.trim() || `${command} exited with code ${code ?? 'unknown'}`));
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export function parseLineModelIds(raw) {
|
|
59
|
+
return raw
|
|
60
|
+
.split(/\r?\n/)
|
|
61
|
+
.map((line) => line.trim())
|
|
62
|
+
.filter(Boolean);
|
|
63
|
+
}
|
|
64
|
+
export function parseJsonModelIds(raw) {
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(raw);
|
|
67
|
+
const entries = Array.isArray(parsed) ? parsed : (parsed.models ?? []);
|
|
68
|
+
return entries
|
|
69
|
+
.map((entry) => entry.id?.trim() || entry.name?.trim() || entry.slug?.trim())
|
|
70
|
+
.filter((id) => Boolean(id));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return parseLineModelIds(raw);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function discoverCursorModels(runCommand) {
|
|
77
|
+
const raw = await runCommand('cursor-agent', ['--list-models'], 2000);
|
|
78
|
+
const fromJson = parseJsonModelIds(raw);
|
|
79
|
+
return fromJson.length > 0 ? fromJson : parseLineModelIds(raw);
|
|
80
|
+
}
|
|
81
|
+
async function discoverCodexModels(runCommand) {
|
|
82
|
+
const raw = await runCommand('codex', ['debug', 'models'], 2000);
|
|
83
|
+
const parsed = parseJsonModelIds(raw);
|
|
84
|
+
if (parsed.length > 0) {
|
|
85
|
+
return parsed;
|
|
86
|
+
}
|
|
87
|
+
return parseLineModelIds(raw).filter((line) => line !== 'visibility=list');
|
|
88
|
+
}
|
|
89
|
+
async function discoverClaudeModels(endpoint, fetchImpl) {
|
|
90
|
+
const apiKey = process.env.ANTHROPIC_API_KEY?.trim() || process.env.BELAY_JUDGE_API_KEY?.trim();
|
|
91
|
+
if (!apiKey) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
const base = (endpoint ?? 'https://api.anthropic.com').replace(/\/$/, '');
|
|
95
|
+
const response = await fetchImpl(`${base}/v1/models`, {
|
|
96
|
+
headers: {
|
|
97
|
+
'x-api-key': apiKey,
|
|
98
|
+
'anthropic-version': '2023-06-01',
|
|
99
|
+
},
|
|
100
|
+
signal: AbortSignal.timeout(5000),
|
|
101
|
+
});
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
const payload = (await response.json());
|
|
106
|
+
return (payload.data ?? [])
|
|
107
|
+
.map((entry) => entry.id?.trim())
|
|
108
|
+
.filter((id) => Boolean(id));
|
|
109
|
+
}
|
|
110
|
+
export async function discoverJudgeModels(input, deps) {
|
|
111
|
+
const { runCommand, fetchImpl, allowCliDiscovery } = resolveDeps(deps);
|
|
112
|
+
const providerId = providerIdFromInput(input.providerId);
|
|
113
|
+
const source = MODEL_DISCOVERY_SOURCES[providerId];
|
|
114
|
+
if (providerId === 'ollama' && input.endpoint) {
|
|
115
|
+
try {
|
|
116
|
+
const response = await fetchImpl(`${input.endpoint.replace(/\/$/, '')}/api/tags`, {
|
|
117
|
+
signal: AbortSignal.timeout(3000),
|
|
118
|
+
});
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
return { source, modelIds: [] };
|
|
121
|
+
}
|
|
122
|
+
const payload = (await response.json());
|
|
123
|
+
const modelIds = (payload.models ?? [])
|
|
124
|
+
.map((entry) => entry.name?.trim())
|
|
125
|
+
.filter((name) => Boolean(name));
|
|
126
|
+
return { source, modelIds };
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return { source, modelIds: [] };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
if (!allowCliDiscovery) {
|
|
134
|
+
return { source, modelIds: [] };
|
|
135
|
+
}
|
|
136
|
+
if (providerId === 'cursor') {
|
|
137
|
+
return { source, modelIds: await discoverCursorModels(runCommand) };
|
|
138
|
+
}
|
|
139
|
+
if (providerId === 'codex') {
|
|
140
|
+
return { source, modelIds: await discoverCodexModels(runCommand) };
|
|
141
|
+
}
|
|
142
|
+
if (providerId === 'claude') {
|
|
143
|
+
return { source, modelIds: await discoverClaudeModels(input.endpoint, fetchImpl) };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return { source, modelIds: [] };
|
|
148
|
+
}
|
|
149
|
+
if (getJudgeProviderSpec(providerId)) {
|
|
150
|
+
return { source, modelIds: [] };
|
|
151
|
+
}
|
|
152
|
+
return { source, modelIds: [] };
|
|
153
|
+
}
|
|
154
|
+
export async function checkJudgeModelPresence(input, deps) {
|
|
155
|
+
const discovery = await discoverJudgeModels(input, deps);
|
|
156
|
+
return modelPresenceFromDiscovery(discovery, input.model);
|
|
157
|
+
}
|
|
158
|
+
export function modelPresenceFromDiscovery(discovery, model) {
|
|
159
|
+
if (discovery.modelIds.length === 0) {
|
|
160
|
+
return { status: 'unverified', source: discovery.source };
|
|
161
|
+
}
|
|
162
|
+
const requested = model.trim();
|
|
163
|
+
const found = discovery.modelIds.some((id) => id === requested || id.startsWith(`${requested}:`));
|
|
164
|
+
return {
|
|
165
|
+
status: found ? 'found' : 'missing',
|
|
166
|
+
source: discovery.source,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function isDeprecatedJudgeModelAuto(model: string | undefined): boolean;
|
|
2
|
+
export declare function rejectDeprecatedJudgeModelAuto(model: string | undefined): void;
|
|
3
|
+
export declare function warnDeprecatedJudgeModelAuto(): void;
|
|
4
|
+
/** @internal test helper */
|
|
5
|
+
export declare function resetDeprecatedJudgeModelAutoWarningForTests(): void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const DEPRECATED_JUDGE_MODEL_AUTO = 'auto';
|
|
2
|
+
export function isDeprecatedJudgeModelAuto(model) {
|
|
3
|
+
return model?.trim().toLowerCase() === DEPRECATED_JUDGE_MODEL_AUTO;
|
|
4
|
+
}
|
|
5
|
+
export function rejectDeprecatedJudgeModelAuto(model) {
|
|
6
|
+
if (isDeprecatedJudgeModelAuto(model)) {
|
|
7
|
+
throw new Error('judge model "auto" is no longer accepted. Use a concrete model id from belay judge list or the provider catalog default.');
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
let warnedDeprecatedAuto = false;
|
|
11
|
+
export function warnDeprecatedJudgeModelAuto() {
|
|
12
|
+
if (warnedDeprecatedAuto) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
warnedDeprecatedAuto = true;
|
|
16
|
+
process.stderr.write('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');
|
|
17
|
+
}
|
|
18
|
+
/** @internal test helper */
|
|
19
|
+
export function resetDeprecatedJudgeModelAutoWarningForTests() {
|
|
20
|
+
warnedDeprecatedAuto = false;
|
|
21
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BelayJudgeConfig } from './config.js';
|
|
2
|
+
import type { Tier1JudgeTransport } from './verdict/judge.js';
|
|
3
|
+
import { type JudgeProviderId } from './verdict/judge-catalog.js';
|
|
4
|
+
export interface JudgeRuntimeCapabilities {
|
|
5
|
+
http: boolean;
|
|
6
|
+
cliTransport: Tier1JudgeTransport | null;
|
|
7
|
+
}
|
|
8
|
+
export declare function detectJudgeRuntimeCapabilities(providerId: JudgeProviderId | string, env?: NodeJS.ProcessEnv): JudgeRuntimeCapabilities;
|
|
9
|
+
export declare function resolveJudgeTransport(judge: Pick<BelayJudgeConfig, 'providerId' | 'provider' | 'model' | 'endpoint'>, env?: NodeJS.ProcessEnv): Tier1JudgeTransport;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { accessSync, constants } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { inferProviderIdFromConfig, normalizeLegacyProviderId, } from './verdict/judge-catalog.js';
|
|
4
|
+
const CLI_COMMANDS = {
|
|
5
|
+
codex: 'codex',
|
|
6
|
+
cursor: 'cursor-agent',
|
|
7
|
+
claude: 'claude',
|
|
8
|
+
};
|
|
9
|
+
function isVitestRuntime(env) {
|
|
10
|
+
return Boolean(env.VITEST || env.VITEST_WORKER_ID);
|
|
11
|
+
}
|
|
12
|
+
function cliTransportForProvider(providerId) {
|
|
13
|
+
if (providerId === 'ollama') {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return `${providerId}-cli`;
|
|
17
|
+
}
|
|
18
|
+
function commandOnPath(command, env) {
|
|
19
|
+
const pathValue = env.PATH ?? '';
|
|
20
|
+
for (const dir of pathValue.split(path.delimiter).filter(Boolean)) {
|
|
21
|
+
const candidate = path.join(dir, command);
|
|
22
|
+
try {
|
|
23
|
+
accessSync(candidate, constants.X_OK);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// try next PATH entry
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
function providerIdFromJudge(judge) {
|
|
33
|
+
if (judge.providerId && normalizeLegacyProviderId(judge.providerId)) {
|
|
34
|
+
return normalizeLegacyProviderId(judge.providerId);
|
|
35
|
+
}
|
|
36
|
+
return inferProviderIdFromConfig(judge);
|
|
37
|
+
}
|
|
38
|
+
export function detectJudgeRuntimeCapabilities(providerId, env = process.env) {
|
|
39
|
+
const normalized = normalizeLegacyProviderId(String(providerId));
|
|
40
|
+
if (!normalized || normalized === 'ollama') {
|
|
41
|
+
return { http: false, cliTransport: null };
|
|
42
|
+
}
|
|
43
|
+
if (env.BELAY_JUDGE_DISABLE_CLI_TRANSPORT === '1') {
|
|
44
|
+
return { http: true, cliTransport: null };
|
|
45
|
+
}
|
|
46
|
+
if (isVitestRuntime(env)) {
|
|
47
|
+
return { http: false, cliTransport: cliTransportForProvider(normalized) };
|
|
48
|
+
}
|
|
49
|
+
const command = CLI_COMMANDS[normalized];
|
|
50
|
+
if (commandOnPath(command, env)) {
|
|
51
|
+
return { http: false, cliTransport: cliTransportForProvider(normalized) };
|
|
52
|
+
}
|
|
53
|
+
return { http: true, cliTransport: null };
|
|
54
|
+
}
|
|
55
|
+
export function resolveJudgeTransport(judge, env = process.env) {
|
|
56
|
+
const providerId = providerIdFromJudge(judge);
|
|
57
|
+
if (providerId === 'ollama') {
|
|
58
|
+
return 'ollama-http';
|
|
59
|
+
}
|
|
60
|
+
if (judge.endpoint?.trim()) {
|
|
61
|
+
return 'http';
|
|
62
|
+
}
|
|
63
|
+
const caps = detectJudgeRuntimeCapabilities(providerId, env);
|
|
64
|
+
if (caps.cliTransport) {
|
|
65
|
+
return caps.cliTransport;
|
|
66
|
+
}
|
|
67
|
+
return 'unavailable';
|
|
68
|
+
}
|
|
@@ -24,21 +24,6 @@ function observedAssessment(evaluation) {
|
|
|
24
24
|
if (evaluation.deletedCount > 0) {
|
|
25
25
|
signals.push('observed_deletions');
|
|
26
26
|
}
|
|
27
|
-
if (evaluation.categories.includes('repo_outside') ||
|
|
28
|
-
evaluation.categories.includes('control_plane') ||
|
|
29
|
-
evaluation.categories.includes('sensitive_path')) {
|
|
30
|
-
return {
|
|
31
|
-
reversibility: 'irreversible',
|
|
32
|
-
external: evaluation.categories.includes('repo_outside'),
|
|
33
|
-
blastRadius: evaluation.categories.includes('control_plane')
|
|
34
|
-
? 'agent-belay control plane'
|
|
35
|
-
: evaluation.categories.includes('repo_outside')
|
|
36
|
-
? 'outside the repository'
|
|
37
|
-
: 'sensitive path',
|
|
38
|
-
confidence: 1,
|
|
39
|
-
signals,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
27
|
if (evaluation.categories.includes('large_deletion')) {
|
|
43
28
|
return {
|
|
44
29
|
reversibility: 'irreversible',
|
|
@@ -66,10 +51,7 @@ export function evaluateTransactionalDiff(changes, ctx) {
|
|
|
66
51
|
categories.add('large_deletion');
|
|
67
52
|
}
|
|
68
53
|
const categoryList = [...categories];
|
|
69
|
-
const dangerous = categoryList.includes('
|
|
70
|
-
categoryList.includes('control_plane') ||
|
|
71
|
-
categoryList.includes('sensitive_path') ||
|
|
72
|
-
categoryList.includes('large_deletion');
|
|
54
|
+
const dangerous = categoryList.includes('large_deletion');
|
|
73
55
|
const base = {
|
|
74
56
|
categories: categoryList,
|
|
75
57
|
changes,
|
package/dist/core/types.d.ts
CHANGED
|
@@ -69,6 +69,8 @@ export interface ClassifierOptions {
|
|
|
69
69
|
fsScopeAllowlist?: FsScopeAllowlistFile;
|
|
70
70
|
/** Test override: inject Tier1 judge without changing config.judge. */
|
|
71
71
|
tier1Judge?: import('./verdict/types.js').Tier1Judge;
|
|
72
|
+
/** When false, path resolution stays fail-closed (opaque cd chains). Default: Boolean(cwd). */
|
|
73
|
+
trustedCwd?: boolean;
|
|
72
74
|
}
|
|
73
75
|
export interface ApprovalRecord {
|
|
74
76
|
approvalId: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BelayConfigV4 } from '../config.js';
|
|
2
2
|
import type { ClassifierOptions, ClassifyResult } from '../types.js';
|
|
3
3
|
import type { Tier1Judge, VerdictContext, VerdictResult } from './types.js';
|
|
4
|
+
export declare function resolveClassifierTrustedCwd(cwd: string, options?: Pick<ClassifierOptions, 'trustedCwd'>, explicit?: boolean): boolean;
|
|
4
5
|
export declare function buildVerdictContext(params: {
|
|
5
6
|
cwd: string;
|
|
6
7
|
repoRoot: string;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { judgeTraceAuditFields } from './judge-audit.js';
|
|
2
2
|
import { createJudgeFromConfig } from './judge-factory.js';
|
|
3
3
|
import { verdict } from './verdict.js';
|
|
4
|
+
export function resolveClassifierTrustedCwd(cwd, options, explicit) {
|
|
5
|
+
if (explicit !== undefined) {
|
|
6
|
+
return explicit;
|
|
7
|
+
}
|
|
8
|
+
return options?.trustedCwd ?? Boolean(cwd);
|
|
9
|
+
}
|
|
4
10
|
export function buildVerdictContext(params) {
|
|
5
11
|
const protectedArtifactRoots = [
|
|
6
12
|
...(params.options?.protectedArtifactRoots ?? []),
|
|
@@ -9,7 +15,7 @@ export function buildVerdictContext(params) {
|
|
|
9
15
|
return {
|
|
10
16
|
cwd: params.cwd,
|
|
11
17
|
repoRoot: params.repoRoot,
|
|
12
|
-
trustedCwd: params.
|
|
18
|
+
trustedCwd: resolveClassifierTrustedCwd(params.cwd, params.options, params.trustedCwd),
|
|
13
19
|
sensitivePaths: params.options?.sensitivePaths ?? params.config.classifier.sensitivePaths,
|
|
14
20
|
protectedArtifactRoots: protectedArtifactRoots.length > 0 ? [...new Set(protectedArtifactRoots)] : undefined,
|
|
15
21
|
customAllowCommands: params.options?.customAllowCommands ?? params.config.overrides.allow,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { isOutsideRepoSecretCredentialPath, isPersistentAgentPath } from './persistent-paths.js';
|
|
1
2
|
import type { VerdictLocation } from './types.js';
|
|
2
3
|
export interface PathTargetAnalysis {
|
|
3
4
|
location: VerdictLocation;
|
|
@@ -8,6 +9,10 @@ export declare function resolveTrustedPath(token: string, trustedCwd: string, tr
|
|
|
8
9
|
export declare function locationForPath(resolvedPath: string | null, repoRoot: string): VerdictLocation;
|
|
9
10
|
export declare function isGitPath(resolvedPath: string, repoRoot: string): boolean;
|
|
10
11
|
export declare function isHighStakesPath(resolvedPath: string, repoRoot: string, sensitivePaths: string[], protectedRoots?: string[]): boolean;
|
|
12
|
+
/** ADR-002 M3: destructive shell head on git/sensitive path (not broad category ask). */
|
|
13
|
+
export declare function isDestructiveMutationHead(head: string): boolean;
|
|
14
|
+
export declare function touchesProtectedRoot(resolvedPath: string, protectedRoots: string[]): boolean;
|
|
15
|
+
export declare function isDestructiveHighStakesMutation(head: string, resolvedPath: string, repoRoot: string, sensitivePaths: string[], protectedRoots?: string[]): boolean;
|
|
11
16
|
export declare function analyzePathTargets(params: {
|
|
12
17
|
targets: string[];
|
|
13
18
|
cwd: string;
|