@guilz-dev/belay 0.3.0 → 0.6.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 +7 -1
- package/dist/adapters/cursor/runtime-entry.js +1 -0
- package/dist/adapters/layouts/scope.js +3 -2
- package/dist/adapters/shared/gate-runtime.d.ts +12 -16
- package/dist/adapters/shared/gate-runtime.js +499 -43
- package/dist/bundle/claude-runtime.mjs +8004 -2676
- package/dist/bundle/codex-runtime.mjs +8021 -2683
- package/dist/bundle/cursor-runtime.mjs +7993 -2667
- package/dist/cli.js +180 -6
- package/dist/commands/approve.d.ts +1 -0
- package/dist/commands/approve.js +45 -26
- package/dist/commands/classify-for-report.js +1 -1
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.js +264 -58
- package/dist/commands/doctor.js +47 -2
- package/dist/commands/explain.js +17 -2
- package/dist/commands/harvest.d.ts +29 -0
- package/dist/commands/harvest.js +72 -0
- package/dist/commands/judge.d.ts +11 -1
- package/dist/commands/judge.js +24 -4
- package/dist/commands/metrics.js +34 -2
- package/dist/commands/quality.d.ts +38 -0
- package/dist/commands/quality.js +105 -0
- package/dist/commands/session.d.ts +20 -0
- package/dist/commands/session.js +51 -0
- package/dist/commands/simulate.d.ts +1 -0
- package/dist/commands/simulate.js +7 -2
- package/dist/commands/standing-allow.d.ts +10 -0
- package/dist/commands/standing-allow.js +26 -0
- package/dist/commands/tui.d.ts +48 -0
- package/dist/commands/tui.js +150 -0
- package/dist/config-io.js +59 -17
- package/dist/conformance/guarantee-posture.d.ts +21 -0
- package/dist/conformance/guarantee-posture.js +46 -0
- package/dist/conformance/guarantee-table.d.ts +1 -0
- package/dist/conformance/guarantee-table.js +14 -9
- package/dist/core/approval-replay-cli.d.ts +7 -0
- package/dist/core/approval-replay-cli.js +36 -0
- package/dist/core/approval-replay.d.ts +45 -0
- package/dist/core/approval-replay.js +141 -0
- package/dist/core/approval-service.d.ts +15 -0
- package/dist/core/approval-service.js +53 -4
- package/dist/core/approval.d.ts +26 -3
- package/dist/core/approval.js +61 -7
- package/dist/core/audit-analysis.d.ts +7 -1
- package/dist/core/audit-analysis.js +111 -0
- package/dist/core/audit-metrics.d.ts +7 -0
- package/dist/core/audit-metrics.js +24 -4
- package/dist/core/audit-query.d.ts +1 -0
- package/dist/core/audit-query.js +3 -0
- package/dist/core/audit-replay-context.d.ts +35 -0
- package/dist/core/audit-replay-context.js +88 -0
- package/dist/core/audit-types.d.ts +24 -1
- package/dist/core/audit-types.js +1 -1
- package/dist/core/capability/approval-state-mutation.d.ts +13 -0
- package/dist/core/capability/approval-state-mutation.js +92 -0
- package/dist/core/capability/approval-v3.d.ts +15 -0
- package/dist/core/capability/approval-v3.js +74 -0
- package/dist/core/capability/attestation.d.ts +13 -0
- package/dist/core/capability/attestation.js +55 -0
- package/dist/core/capability/boundary-attestation-sign.d.ts +21 -0
- package/dist/core/capability/boundary-attestation-sign.js +84 -0
- package/dist/core/capability/boundary-driver-container.d.ts +7 -0
- package/dist/core/capability/boundary-driver-container.js +122 -0
- package/dist/core/capability/boundary-driver.d.ts +29 -0
- package/dist/core/capability/boundary-driver.js +43 -0
- package/dist/core/capability/boundary-egress.d.ts +21 -0
- package/dist/core/capability/boundary-egress.js +109 -0
- package/dist/core/capability/boundary-grant-materialize.d.ts +13 -0
- package/dist/core/capability/boundary-grant-materialize.js +78 -0
- package/dist/core/capability/boundary-profile.d.ts +15 -0
- package/dist/core/capability/boundary-profile.js +25 -0
- package/dist/core/capability/boundary-run.d.ts +23 -0
- package/dist/core/capability/boundary-run.js +23 -0
- package/dist/core/capability/boundary-session.d.ts +57 -0
- package/dist/core/capability/boundary-session.js +129 -0
- package/dist/core/capability/capability-request-hash.d.ts +2 -0
- package/dist/core/capability/capability-request-hash.js +8 -0
- package/dist/core/capability/gate-latency-ratchet.d.ts +7 -0
- package/dist/core/capability/gate-latency-ratchet.js +18 -0
- package/dist/core/capability/gate-policy-shadow.d.ts +25 -0
- package/dist/core/capability/gate-policy-shadow.js +108 -0
- package/dist/core/capability/gate-shadow-audit.d.ts +1 -0
- package/dist/core/capability/gate-shadow-audit.js +1 -0
- package/dist/core/capability/gate-shadow-ratchet.d.ts +14 -0
- package/dist/core/capability/gate-shadow-ratchet.js +81 -0
- package/dist/core/capability/grant-lease.d.ts +9 -0
- package/dist/core/capability/grant-lease.js +46 -0
- package/dist/core/capability/grant-loader.d.ts +12 -0
- package/dist/core/capability/grant-loader.js +23 -0
- package/dist/core/capability/grant-match.d.ts +6 -0
- package/dist/core/capability/grant-match.js +93 -0
- package/dist/core/capability/grant.d.ts +16 -0
- package/dist/core/capability/grant.js +16 -0
- package/dist/core/capability/index.d.ts +10 -1
- package/dist/core/capability/index.js +8 -0
- package/dist/core/capability/limits.d.ts +7 -0
- package/dist/core/capability/limits.js +55 -0
- package/dist/core/capability/paths.d.ts +2 -2
- package/dist/core/capability/paths.js +9 -9
- package/dist/core/capability/policy-bridge.d.ts +18 -0
- package/dist/core/capability/policy-bridge.js +51 -0
- package/dist/core/capability/policy-engine.d.ts +70 -0
- package/dist/core/capability/policy-engine.js +476 -0
- package/dist/core/capability/policy-types.d.ts +20 -0
- package/dist/core/capability/policy-types.js +1 -0
- package/dist/core/capability/request.d.ts +45 -0
- package/dist/core/capability/request.js +1 -0
- package/dist/core/capability/resolver.d.ts +4 -0
- package/dist/core/capability/resolver.js +4 -0
- package/dist/core/capability/trusted-workspace-roots.d.ts +25 -0
- package/dist/core/capability/trusted-workspace-roots.js +149 -0
- package/dist/core/capability/types.d.ts +11 -1
- package/dist/core/capability-approval.d.ts +2 -1
- package/dist/core/capability-approval.js +100 -2
- package/dist/core/classify-subagent.d.ts +2 -1
- package/dist/core/classify-subagent.js +40 -22
- package/dist/core/classify-tool.js +157 -64
- package/dist/core/config.d.ts +32 -3
- package/dist/core/config.js +105 -3
- package/dist/core/gate-contract.d.ts +3 -0
- package/dist/core/gate-contract.js +3 -0
- package/dist/core/gate-engine.js +12 -7
- package/dist/core/harvest.d.ts +53 -0
- package/dist/core/harvest.js +276 -0
- package/dist/core/index.d.ts +9 -3
- package/dist/core/index.js +6 -2
- package/dist/core/judge-doctor.d.ts +3 -0
- package/dist/core/judge-doctor.js +90 -11
- package/dist/core/judge-fallback-hints.d.ts +13 -0
- package/dist/core/judge-fallback-hints.js +108 -0
- package/dist/core/path-utils.d.ts +12 -0
- package/dist/core/path-utils.js +60 -7
- package/dist/core/reclassify.d.ts +3 -0
- package/dist/core/reclassify.js +44 -14
- package/dist/core/recovery/capability.d.ts +3 -0
- package/dist/core/recovery/capability.js +22 -0
- package/dist/core/recovery/fail-closed.d.ts +6 -0
- package/dist/core/recovery/fail-closed.js +25 -0
- package/dist/core/recovery/index.d.ts +3 -0
- package/dist/core/recovery/index.js +2 -0
- package/dist/core/recovery/types.d.ts +20 -0
- package/dist/core/recovery/types.js +1 -0
- package/dist/core/replay-scrub.d.ts +9 -0
- package/dist/core/replay-scrub.js +43 -0
- package/dist/core/shell-tokenizer.d.ts +2 -0
- package/dist/core/shell-tokenizer.js +82 -23
- package/dist/core/standing-allow.d.ts +50 -0
- package/dist/core/standing-allow.js +178 -0
- package/dist/core/transactional/diff-evaluator.js +4 -1
- package/dist/core/transactional/eligibility.js +4 -0
- package/dist/core/transactional/git-worktree.d.ts +10 -3
- package/dist/core/transactional/git-worktree.js +81 -11
- package/dist/core/transactional/reasons.d.ts +1 -0
- package/dist/core/transactional/reasons.js +4 -0
- package/dist/core/transactional/runner.js +22 -6
- package/dist/core/transactional/types.d.ts +4 -0
- package/dist/core/types.d.ts +36 -3
- package/dist/core/verdict/adapter.d.ts +3 -4
- package/dist/core/verdict/adapter.js +19 -6
- package/dist/core/verdict/containment.d.ts +4 -3
- package/dist/core/verdict/containment.js +17 -12
- package/dist/core/verdict/cursor-acp-client.d.ts +67 -0
- package/dist/core/verdict/cursor-acp-client.js +345 -0
- package/dist/core/verdict/egress-classify.d.ts +2 -2
- package/dist/core/verdict/egress-classify.js +2 -2
- package/dist/core/verdict/git-classifier.d.ts +26 -0
- package/dist/core/verdict/git-classifier.js +533 -0
- package/dist/core/verdict/judge-audit.d.ts +1 -0
- package/dist/core/verdict/judge-audit.js +33 -1
- package/dist/core/verdict/judge-baseline.d.ts +20 -0
- package/dist/core/verdict/judge-baseline.js +49 -0
- package/dist/core/verdict/judge-broker-service.d.ts +42 -0
- package/dist/core/verdict/judge-broker-service.js +291 -0
- package/dist/core/verdict/judge-catalog.js +1 -0
- package/dist/core/verdict/judge-cli-fingerprint.d.ts +6 -0
- package/dist/core/verdict/judge-cli-fingerprint.js +67 -0
- package/dist/core/verdict/judge-cli.d.ts +23 -0
- package/dist/core/verdict/judge-cli.js +139 -16
- package/dist/core/verdict/judge-factory.js +13 -4
- package/dist/core/verdict/judge-provider-matrix.d.ts +20 -0
- package/dist/core/verdict/judge-provider-matrix.js +66 -0
- package/dist/core/verdict/judge-runtime-config.d.ts +44 -0
- package/dist/core/verdict/judge-runtime-config.js +96 -0
- package/dist/core/verdict/judge-session-broker.d.ts +48 -0
- package/dist/core/verdict/judge-session-broker.js +200 -0
- package/dist/core/verdict/judge-session-guard.d.ts +27 -0
- package/dist/core/verdict/judge-session-guard.js +91 -0
- package/dist/core/verdict/judge-session-kill-switch.d.ts +11 -0
- package/dist/core/verdict/judge-session-kill-switch.js +43 -0
- package/dist/core/verdict/judge-session-mutex.d.ts +9 -0
- package/dist/core/verdict/judge-session-mutex.js +23 -0
- package/dist/core/verdict/judge-shadow.d.ts +24 -0
- package/dist/core/verdict/judge-shadow.js +116 -0
- package/dist/core/verdict/judge-transport.d.ts +44 -0
- package/dist/core/verdict/judge-transport.js +268 -0
- package/dist/core/verdict/judge.d.ts +14 -14
- package/dist/core/verdict/judge.js +5 -67
- package/dist/core/verdict/launcher-resolve.d.ts +2 -0
- package/dist/core/verdict/launcher-resolve.js +16 -0
- package/dist/core/verdict/parser.js +6 -1
- package/dist/core/verdict/prescan.d.ts +15 -0
- package/dist/core/verdict/prescan.js +68 -0
- package/dist/core/verdict/shell-policy.d.ts +25 -0
- package/dist/core/verdict/shell-policy.js +40 -0
- package/dist/core/verdict/shell-semantics.d.ts +24 -0
- package/dist/core/verdict/shell-semantics.js +225 -0
- package/dist/core/verdict/types.d.ts +26 -1
- package/dist/core/verdict/verdict.js +300 -179
- package/dist/corpus/adversarial-probe.d.ts +86 -0
- package/dist/corpus/adversarial-probe.js +220 -0
- package/dist/corpus/evaluate.d.ts +45 -12
- package/dist/corpus/evaluate.js +61 -8
- package/dist/corpus/gate-latency-budget.d.ts +39 -0
- package/dist/corpus/gate-latency-budget.js +33 -0
- package/dist/corpus/gates.d.ts +35 -0
- package/dist/corpus/gates.js +81 -0
- package/dist/corpus/judge-accuracy.d.ts +8 -0
- package/dist/corpus/judge-accuracy.js +39 -0
- package/dist/corpus/must-allow-commands.d.ts +5 -0
- package/dist/corpus/must-allow-commands.js +13 -0
- package/dist/corpus/mutators.d.ts +28 -0
- package/dist/corpus/mutators.js +162 -0
- package/dist/corpus/ratchet.d.ts +42 -0
- package/dist/corpus/ratchet.js +116 -0
- package/dist/corpus/runtime-match.d.ts +19 -0
- package/dist/corpus/runtime-match.js +43 -0
- package/dist/corpus/standing-allow-catalog.generated.d.ts +13 -0
- package/dist/corpus/standing-allow-catalog.generated.js +99 -0
- package/dist/corpus/types.d.ts +47 -0
- package/dist/corpus/types.js +157 -0
- package/dist/installer.js +4 -1
- package/dist/judge-broker-daemon.d.ts +1 -0
- package/dist/judge-broker-daemon.js +137 -0
- package/dist/services/sandbox-service.d.ts +5 -0
- package/dist/services/sandbox-service.js +30 -2
- package/dist/types.d.ts +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -3
- package/skills/belay/SKILL.md +7 -1
- package/skills/belay/belay-approve.md +17 -0
package/dist/commands/config.js
CHANGED
|
@@ -8,11 +8,14 @@ import { clearJudgeCredentialStore, writeJudgeCredentialStore } from '../core/cr
|
|
|
8
8
|
import { refreshIntegrityIfPinned } from '../core/integrity.js';
|
|
9
9
|
import { defaultJudgeProviderForAdapter, hasValidCloudConsent, isCloudJudgeConfig, resolveJudgeUsePatch, } from '../core/judge-config.js';
|
|
10
10
|
import { rejectDeprecatedJudgeModelAuto } from '../core/judge-model-policy.js';
|
|
11
|
-
import {
|
|
11
|
+
import { detectJudgeRuntimeCapabilities, resolveJudgeTransport, } from '../core/judge-runtime-detection.js';
|
|
12
|
+
import { getJudgeProviderSpec, isJudgeProviderId, JUDGE_PROVIDER_IDS, normalizeLegacyProviderId, } from '../core/verdict/judge-catalog.js';
|
|
13
|
+
import { normalizeJudgeRuntimeConfig } from '../core/verdict/judge-runtime-config.js';
|
|
12
14
|
import { initProject } from '../installer.js';
|
|
13
15
|
import { isBelayFloorInstalled } from './health-snapshot.js';
|
|
14
16
|
import { judgeStatus } from './judge.js';
|
|
15
17
|
import { readKeyFromStdin } from './stdin-key.js';
|
|
18
|
+
import { confirmPrompt, intro, isInteractiveTTY, selectPrompt } from './tui.js';
|
|
16
19
|
export const BELAY_CONFIG_SUBCOMMANDS = [
|
|
17
20
|
'list',
|
|
18
21
|
'get',
|
|
@@ -29,6 +32,8 @@ const JUDGE_CONFIG_PATHS = [
|
|
|
29
32
|
'judge.timeoutMs',
|
|
30
33
|
'judge.credential.mode',
|
|
31
34
|
'judge.credential.ref',
|
|
35
|
+
'judge.runtime.session.enabled',
|
|
36
|
+
'judge.runtime.shadow.enabled',
|
|
32
37
|
];
|
|
33
38
|
export function parseAdapter(value) {
|
|
34
39
|
const normalized = (value?.trim() || 'cursor').toLowerCase();
|
|
@@ -96,6 +101,12 @@ function getJudgeField(judge, pathKey) {
|
|
|
96
101
|
return judge.credential?.mode ?? null;
|
|
97
102
|
if (pathKey === 'judge.credential.ref')
|
|
98
103
|
return judge.credential?.ref ?? null;
|
|
104
|
+
if (pathKey === 'judge.runtime.session.enabled') {
|
|
105
|
+
return normalizeJudgeRuntimeConfig(judge.runtime).session.enabled;
|
|
106
|
+
}
|
|
107
|
+
if (pathKey === 'judge.runtime.shadow.enabled') {
|
|
108
|
+
return normalizeJudgeRuntimeConfig(judge.runtime).shadow.enabled;
|
|
109
|
+
}
|
|
99
110
|
throw new Error(`Unknown judge config path: ${pathKey}`);
|
|
100
111
|
}
|
|
101
112
|
function listJudgeFields(judge) {
|
|
@@ -106,7 +117,9 @@ function listJudgeFields(judge) {
|
|
|
106
117
|
return entries;
|
|
107
118
|
}
|
|
108
119
|
function warnCloudConsentIfNeeded(judge) {
|
|
109
|
-
if (isCloudJudgeConfig(judge) &&
|
|
120
|
+
if (isCloudJudgeConfig(judge) &&
|
|
121
|
+
resolveJudgeTransport(judge) === 'http' &&
|
|
122
|
+
!hasValidCloudConsent(judge)) {
|
|
110
123
|
process.stderr.write('Warning: Cloud judge saved without recorded consent. Tier1 cloud judge will fail closed until consent is granted (belay judge consent + belay approve, or TTY --accept-cloud-judge).\n');
|
|
111
124
|
}
|
|
112
125
|
}
|
|
@@ -177,6 +190,36 @@ async function applyJudgeSet(repoRoot, config, pathKey, rawValue) {
|
|
|
177
190
|
await persistJudge(repoRoot, config, judge, adapter);
|
|
178
191
|
return judge;
|
|
179
192
|
}
|
|
193
|
+
if (pathKey === 'judge.runtime.session.enabled') {
|
|
194
|
+
if (value !== 'true' && value !== 'false') {
|
|
195
|
+
throw new Error('judge.runtime.session.enabled must be true or false.');
|
|
196
|
+
}
|
|
197
|
+
const runtime = normalizeJudgeRuntimeConfig(config.judge.runtime);
|
|
198
|
+
const judge = normalizeJudgeConfig({
|
|
199
|
+
...config.judge,
|
|
200
|
+
runtime: {
|
|
201
|
+
...runtime,
|
|
202
|
+
session: { ...runtime.session, enabled: value === 'true' },
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
await persistJudge(repoRoot, config, judge, adapter);
|
|
206
|
+
return judge;
|
|
207
|
+
}
|
|
208
|
+
if (pathKey === 'judge.runtime.shadow.enabled') {
|
|
209
|
+
if (value !== 'true' && value !== 'false') {
|
|
210
|
+
throw new Error('judge.runtime.shadow.enabled must be true or false.');
|
|
211
|
+
}
|
|
212
|
+
const runtime = normalizeJudgeRuntimeConfig(config.judge.runtime);
|
|
213
|
+
const judge = normalizeJudgeConfig({
|
|
214
|
+
...config.judge,
|
|
215
|
+
runtime: {
|
|
216
|
+
...runtime,
|
|
217
|
+
shadow: { ...runtime.shadow, enabled: value === 'true' },
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
await persistJudge(repoRoot, config, judge, adapter);
|
|
221
|
+
return judge;
|
|
222
|
+
}
|
|
180
223
|
throw new Error(`Unknown judge config path: ${pathKey}`);
|
|
181
224
|
}
|
|
182
225
|
async function applyJudgeUnset(repoRoot, config, pathKey) {
|
|
@@ -285,46 +328,196 @@ export async function runBelayConfigCredential(options) {
|
|
|
285
328
|
}
|
|
286
329
|
throw new Error('credential requires action: mode, set, or clear.');
|
|
287
330
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
331
|
+
export const JUDGE_CREDENTIAL_MODE_PROMPT = 'Judge API key source';
|
|
332
|
+
export const JUDGE_CREDENTIAL_STORE_KEY_PROMPT = 'Paste API key to store locally (visible input; prefer belay config credential set --key-stdin): ';
|
|
333
|
+
export const JUDGE_TRANSPORT_MODE_PROMPT = 'How should Belay reach the judge?';
|
|
334
|
+
export const JUDGE_HTTP_ENDPOINT_PROMPT = 'Judge HTTP API URL:';
|
|
335
|
+
const CLOUD_CLI_LABELS = {
|
|
336
|
+
codex: 'Codex CLI',
|
|
337
|
+
claude: 'Claude CLI',
|
|
338
|
+
cursor: 'Cursor CLI',
|
|
339
|
+
};
|
|
340
|
+
export function buildJudgeTransportSelectOptions(judgeProviderId) {
|
|
341
|
+
const cliLabel = CLOUD_CLI_LABELS[judgeProviderId];
|
|
342
|
+
const runtime = detectJudgeRuntimeCapabilities(judgeProviderId);
|
|
343
|
+
const cliHint = runtime.cliTransport
|
|
344
|
+
? 'Uses your logged-in host session; no URL needed.'
|
|
345
|
+
: 'Host CLI not found on PATH — install it or choose custom HTTP API.';
|
|
346
|
+
return {
|
|
347
|
+
message: JUDGE_TRANSPORT_MODE_PROMPT,
|
|
348
|
+
defaultValue: 'cli',
|
|
349
|
+
choices: [
|
|
350
|
+
{
|
|
351
|
+
value: 'cli',
|
|
352
|
+
label: `Use ${cliLabel} (recommended)`,
|
|
353
|
+
hint: cliHint,
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
value: 'http',
|
|
357
|
+
label: 'Custom HTTP API endpoint',
|
|
358
|
+
hint: 'Requires a provider API URL and cloud egress consent.',
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
export function buildJudgeCredentialModeSelectOptions(judgeProviderId) {
|
|
364
|
+
const spec = getJudgeProviderSpec(judgeProviderId);
|
|
365
|
+
const providerEnvVars = spec?.apiKeyEnvVars.filter((name) => name !== 'BELAY_JUDGE_API_KEY') ?? [];
|
|
366
|
+
const envNames = providerEnvVars.length > 0
|
|
367
|
+
? ['BELAY_JUDGE_API_KEY', ...providerEnvVars].join(', ')
|
|
368
|
+
: 'BELAY_JUDGE_API_KEY';
|
|
369
|
+
const projectHint = providerEnvVars.length > 0
|
|
370
|
+
? `Uses ${envNames} from the shell; does not read .env files. Or host CLI login when available.`
|
|
371
|
+
: 'Uses BELAY_JUDGE_API_KEY from the shell; does not read .env files.';
|
|
372
|
+
return {
|
|
373
|
+
message: JUDGE_CREDENTIAL_MODE_PROMPT,
|
|
374
|
+
defaultValue: 'project',
|
|
375
|
+
choices: [
|
|
376
|
+
{
|
|
377
|
+
value: 'project',
|
|
378
|
+
label: 'Environment variables or host CLI',
|
|
379
|
+
hint: projectHint,
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
value: 'apiKey',
|
|
383
|
+
label: 'Store in Belay credential store',
|
|
384
|
+
hint: 'Saved to credentials.json (mode 0600) on the next step.',
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function formatSelectPrompt(options) {
|
|
390
|
+
const hasDetails = options.choices.some((choice) => choice.hint || choice.label);
|
|
391
|
+
if (hasDetails) {
|
|
392
|
+
const lines = options.choices.map((choice) => {
|
|
393
|
+
const label = choice.label ?? choice.value;
|
|
394
|
+
const hint = choice.hint ? ` — ${choice.hint}` : '';
|
|
395
|
+
return ` ${choice.value}: ${label}${hint}`;
|
|
396
|
+
});
|
|
397
|
+
const values = options.choices.map((choice) => choice.value).join(' | ');
|
|
398
|
+
return `${options.message}\n${lines.join('\n')}\n[${values}] (${options.defaultValue}): `;
|
|
291
399
|
}
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
400
|
+
const values = options.choices.map((choice) => choice.value).join(' | ');
|
|
401
|
+
return `${options.message} [${values}] (${options.defaultValue}): `;
|
|
402
|
+
}
|
|
403
|
+
function formatConfirmPrompt(message, defaultValue) {
|
|
404
|
+
return `${message} [y | n] (${defaultValue ? 'y' : 'n'}): `;
|
|
405
|
+
}
|
|
406
|
+
function parseSelectAnswer(raw, options) {
|
|
407
|
+
const normalized = raw.trim().toLowerCase();
|
|
408
|
+
if (!normalized) {
|
|
409
|
+
return options.defaultValue;
|
|
410
|
+
}
|
|
411
|
+
const found = options.choices.find((choice) => {
|
|
412
|
+
if (choice.value.toLowerCase() === normalized) {
|
|
413
|
+
return true;
|
|
301
414
|
}
|
|
415
|
+
return choice.label?.trim().toLowerCase() === normalized;
|
|
416
|
+
});
|
|
417
|
+
if (!found) {
|
|
418
|
+
throw new Error(`Invalid choice for ${options.message}: ${raw}`);
|
|
302
419
|
}
|
|
303
|
-
|
|
304
|
-
if (judgeEndpoint) {
|
|
305
|
-
acceptCloud = parseYesNo(await ask('Accept cloud judge egress (redacted commands leave the repo)? [y | n] (n): '), false);
|
|
306
|
-
}
|
|
307
|
-
return { judgeCredentialMode, judgeEndpoint, acceptCloud };
|
|
420
|
+
return found.value;
|
|
308
421
|
}
|
|
309
|
-
async function
|
|
310
|
-
if (prompts) {
|
|
311
|
-
let index = 0;
|
|
312
|
-
const ask = async (message) => {
|
|
313
|
-
if (index >= prompts.length) {
|
|
314
|
-
throw new Error(`unexpected config prompt: ${message}`);
|
|
315
|
-
}
|
|
316
|
-
return prompts[index++];
|
|
317
|
-
};
|
|
318
|
-
return fn(ask);
|
|
319
|
-
}
|
|
422
|
+
async function askTextLazy(message) {
|
|
320
423
|
const rl = readline.createInterface({ input, output });
|
|
321
424
|
try {
|
|
322
|
-
return await
|
|
425
|
+
return (await rl.question(message)).trimEnd();
|
|
323
426
|
}
|
|
324
427
|
finally {
|
|
325
428
|
rl.close();
|
|
326
429
|
}
|
|
327
430
|
}
|
|
431
|
+
function createTestPrompter(prompts) {
|
|
432
|
+
let index = 0;
|
|
433
|
+
const nextPrompt = (message) => {
|
|
434
|
+
if (index >= prompts.length) {
|
|
435
|
+
throw new Error(`unexpected config prompt: ${message}`);
|
|
436
|
+
}
|
|
437
|
+
return prompts[index++];
|
|
438
|
+
};
|
|
439
|
+
return {
|
|
440
|
+
askText: async (message) => nextPrompt(message),
|
|
441
|
+
askSelect: async (options, parseRaw) => {
|
|
442
|
+
const raw = nextPrompt(formatSelectPrompt(options));
|
|
443
|
+
return parseRaw ? parseRaw(raw, options) : parseSelectAnswer(raw, options);
|
|
444
|
+
},
|
|
445
|
+
askConfirm: async (message, defaultValue) => parseYesNo(nextPrompt(formatConfirmPrompt(message, defaultValue)), defaultValue),
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function createReadlinePrompter() {
|
|
449
|
+
return {
|
|
450
|
+
askText: askTextLazy,
|
|
451
|
+
askSelect: async (options, parseRaw) => {
|
|
452
|
+
const raw = await askTextLazy(formatSelectPrompt(options));
|
|
453
|
+
return parseRaw ? parseRaw(raw, options) : parseSelectAnswer(raw, options);
|
|
454
|
+
},
|
|
455
|
+
askConfirm: async (message, defaultValue) => parseYesNo(await askTextLazy(formatConfirmPrompt(message, defaultValue)), defaultValue),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function createTuiPrompter() {
|
|
459
|
+
return {
|
|
460
|
+
askText: askTextLazy,
|
|
461
|
+
askSelect: selectPrompt,
|
|
462
|
+
askConfirm: confirmPrompt,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
async function withConfigWizardPrompter(options, fn) {
|
|
466
|
+
if (options.prompts) {
|
|
467
|
+
return fn(createTestPrompter(options.prompts));
|
|
468
|
+
}
|
|
469
|
+
if (isInteractiveTTY()) {
|
|
470
|
+
return fn(createTuiPrompter());
|
|
471
|
+
}
|
|
472
|
+
return fn(createReadlinePrompter());
|
|
473
|
+
}
|
|
474
|
+
function writeConfigWizardBanner(options, title) {
|
|
475
|
+
if (options.skipBanner) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (isInteractiveTTY()) {
|
|
479
|
+
intro(title);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
output.write(`${title}\n`);
|
|
483
|
+
}
|
|
484
|
+
async function askJudgeHttpEndpoint(prompter) {
|
|
485
|
+
while (true) {
|
|
486
|
+
const endpoint = (await prompter.askText(JUDGE_HTTP_ENDPOINT_PROMPT)).trim();
|
|
487
|
+
if (endpoint) {
|
|
488
|
+
return endpoint;
|
|
489
|
+
}
|
|
490
|
+
process.stderr.write('Warning: HTTP endpoint URL is required when using custom HTTP API. Paste a URL or re-run belay config and choose host CLI.\n');
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
async function askJudgeCredentialStoreKey(prompter) {
|
|
494
|
+
while (true) {
|
|
495
|
+
const key = (await prompter.askText(JUDGE_CREDENTIAL_STORE_KEY_PROMPT)).trim();
|
|
496
|
+
if (key) {
|
|
497
|
+
return key;
|
|
498
|
+
}
|
|
499
|
+
process.stderr.write('Warning: API key cannot be empty. Paste a key or re-run belay config and choose environment variables.\n');
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
async function collectCloudJudgeWizardAnswers(prompter, judgeProviderId) {
|
|
503
|
+
if (judgeProviderId === 'ollama') {
|
|
504
|
+
return { acceptCloud: false };
|
|
505
|
+
}
|
|
506
|
+
const judgeCredentialMode = await prompter.askSelect(buildJudgeCredentialModeSelectOptions(judgeProviderId));
|
|
507
|
+
const transportMode = await prompter.askSelect(buildJudgeTransportSelectOptions(judgeProviderId));
|
|
508
|
+
let judgeEndpoint;
|
|
509
|
+
if (transportMode === 'http') {
|
|
510
|
+
judgeEndpoint = await askJudgeHttpEndpoint(prompter);
|
|
511
|
+
}
|
|
512
|
+
if (judgeCredentialMode === 'apiKey') {
|
|
513
|
+
process.env.BELAY_CONFIG_WIZARD_JUDGE_KEY = await askJudgeCredentialStoreKey(prompter);
|
|
514
|
+
}
|
|
515
|
+
let acceptCloud = false;
|
|
516
|
+
if (judgeEndpoint) {
|
|
517
|
+
acceptCloud = await prompter.askConfirm('Accept cloud judge egress (redacted commands leave the repo)?', false);
|
|
518
|
+
}
|
|
519
|
+
return { judgeCredentialMode, judgeEndpoint, acceptCloud };
|
|
520
|
+
}
|
|
328
521
|
export async function resolveBelayConfigInteractiveMode(repoRoot) {
|
|
329
522
|
try {
|
|
330
523
|
return (await isBelayFloorInstalled({ targetDir: repoRoot })) ? 'judge-only' : 'full';
|
|
@@ -333,16 +526,26 @@ export async function resolveBelayConfigInteractiveMode(repoRoot) {
|
|
|
333
526
|
return 'full';
|
|
334
527
|
}
|
|
335
528
|
}
|
|
336
|
-
async function
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
529
|
+
async function runBelayConfigFullWithWizard(prompter, options) {
|
|
530
|
+
writeConfigWizardBanner(options, 'belay config');
|
|
531
|
+
const adapter = await prompter.askSelect({
|
|
532
|
+
message: 'Adapter',
|
|
533
|
+
defaultValue: 'cursor',
|
|
534
|
+
choices: [{ value: 'cursor' }, { value: 'claude' }, { value: 'codex' }],
|
|
535
|
+
});
|
|
536
|
+
const scope = await prompter.askSelect({
|
|
537
|
+
message: 'Install scope',
|
|
538
|
+
defaultValue: 'project',
|
|
539
|
+
choices: [{ value: 'project' }, { value: 'global' }],
|
|
540
|
+
});
|
|
541
|
+
const withSkill = await prompter.askConfirm('Install SKILL.md and slash commands?', true);
|
|
343
542
|
const defaultJudgeProviderId = defaultJudgeProviderForAdapter(adapter);
|
|
344
|
-
const judgeProviderId =
|
|
345
|
-
|
|
543
|
+
const judgeProviderId = await prompter.askSelect({
|
|
544
|
+
message: 'Judge provider',
|
|
545
|
+
defaultValue: defaultJudgeProviderId,
|
|
546
|
+
choices: JUDGE_PROVIDER_IDS.map((providerId) => ({ value: providerId })),
|
|
547
|
+
}, (raw, selectOptions) => parseJudgeProviderId(raw, selectOptions.defaultValue));
|
|
548
|
+
const { judgeCredentialMode, judgeEndpoint, acceptCloud } = await collectCloudJudgeWizardAnswers(prompter, judgeProviderId);
|
|
346
549
|
const initOptions = buildInitOptionsFromConfigAnswers({
|
|
347
550
|
adapter,
|
|
348
551
|
scope,
|
|
@@ -362,19 +565,22 @@ async function runBelayConfigFullWithPrompter(ask, options) {
|
|
|
362
565
|
}
|
|
363
566
|
return result;
|
|
364
567
|
}
|
|
365
|
-
async function
|
|
366
|
-
|
|
367
|
-
output.write('belay config (judge only)\n');
|
|
368
|
-
}
|
|
568
|
+
async function runBelayConfigJudgeOnlyWithWizard(prompter, options, repoRoot, config, adapter) {
|
|
569
|
+
writeConfigWizardBanner(options, 'belay config (judge only)');
|
|
369
570
|
const defaultJudgeProviderId = defaultJudgeProviderForAdapter(adapter);
|
|
370
|
-
const judgeProviderId =
|
|
371
|
-
|
|
571
|
+
const judgeProviderId = await prompter.askSelect({
|
|
572
|
+
message: 'Judge provider',
|
|
573
|
+
defaultValue: defaultJudgeProviderId,
|
|
574
|
+
choices: JUDGE_PROVIDER_IDS.map((providerId) => ({ value: providerId })),
|
|
575
|
+
}, (raw, selectOptions) => parseJudgeProviderId(raw, selectOptions.defaultValue));
|
|
576
|
+
const { judgeCredentialMode, judgeEndpoint, acceptCloud } = await collectCloudJudgeWizardAnswers(prompter, judgeProviderId);
|
|
372
577
|
const patch = resolveJudgeUsePatch(config.judge, {
|
|
373
578
|
providerId: judgeProviderId,
|
|
374
579
|
endpoint: judgeEndpoint,
|
|
375
580
|
credentialMode: judgeCredentialMode,
|
|
376
581
|
acceptCloud: acceptCloud && Boolean(judgeEndpoint),
|
|
377
|
-
|
|
582
|
+
// `prompts` is a scripted stand-in for interactive responses in tests.
|
|
583
|
+
interactiveTTY: isInteractiveTTY() || Boolean(options.prompts),
|
|
378
584
|
interactiveConsentApproved: acceptCloud && Boolean(judgeEndpoint),
|
|
379
585
|
});
|
|
380
586
|
if (patch.errors.length > 0) {
|
|
@@ -399,13 +605,13 @@ async function runBelayConfigJudgeOnlyWithPrompter(ask, options, repoRoot, confi
|
|
|
399
605
|
return { repoRoot, adapter };
|
|
400
606
|
}
|
|
401
607
|
export async function runBelayConfigFullInteractive(options = {}) {
|
|
402
|
-
return
|
|
608
|
+
return withConfigWizardPrompter(options, (prompter) => runBelayConfigFullWithWizard(prompter, options));
|
|
403
609
|
}
|
|
404
610
|
export async function runBelayConfigJudgeOnlyInteractive(options = {}) {
|
|
405
611
|
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
406
612
|
const config = await loadConfigFile(repoRoot);
|
|
407
613
|
const adapter = resolveAdapterName(config);
|
|
408
|
-
return
|
|
614
|
+
return withConfigWizardPrompter(options, (prompter) => runBelayConfigJudgeOnlyWithWizard(prompter, options, repoRoot, config, adapter));
|
|
409
615
|
}
|
|
410
616
|
export async function runBelayConfigInteractive(options = {}) {
|
|
411
617
|
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
@@ -427,16 +633,16 @@ export async function runBelayConfigInteractive(options = {}) {
|
|
|
427
633
|
skipBanner: true,
|
|
428
634
|
});
|
|
429
635
|
}
|
|
430
|
-
return
|
|
431
|
-
|
|
432
|
-
const judgeOnly =
|
|
433
|
-
if (judgeOnly) {
|
|
434
|
-
|
|
435
|
-
const adapter = resolveAdapterName(config);
|
|
436
|
-
return runBelayConfigJudgeOnlyWithPrompter(ask, { ...options, skipBanner: true }, repoRoot, config, adapter);
|
|
636
|
+
return withConfigWizardPrompter(options, async (prompter) => {
|
|
637
|
+
writeConfigWizardBanner(options, 'belay config');
|
|
638
|
+
const judgeOnly = await prompter.askConfirm('Configure judge only?', true);
|
|
639
|
+
if (!judgeOnly) {
|
|
640
|
+
return runBelayConfigFullWithWizard(prompter, { ...options, skipBanner: true });
|
|
437
641
|
}
|
|
438
|
-
|
|
439
|
-
|
|
642
|
+
const config = await loadConfigFile(repoRoot);
|
|
643
|
+
const adapter = resolveAdapterName(config);
|
|
644
|
+
return runBelayConfigJudgeOnlyWithWizard(prompter, { ...options, skipBanner: true }, repoRoot, config, adapter);
|
|
645
|
+
});
|
|
440
646
|
}
|
|
441
647
|
return runBelayConfigFullInteractive(options);
|
|
442
648
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -8,9 +8,12 @@ import { resolveScopedPaths } from '../adapters/layouts/scope.js';
|
|
|
8
8
|
import { cleanupOrphanApprovalState } from '../cleanup-orphans.js';
|
|
9
9
|
import { approvedApprovalsPath, belayStateDir, detectAdapterName, loadLayeredConfig, pendingApprovalsPath, repoLocalStateDirFor, } from '../config-io.js';
|
|
10
10
|
import { detectFenceDrift, summarizeAuditVisibility } from '../core/audit-summary.js';
|
|
11
|
+
import { inspectBoundaryAttestationFile } from '../core/capability/boundary-attestation-sign.js';
|
|
12
|
+
import { boundaryAttestationPath, boundarySessionStatus, } from '../core/capability/boundary-session.js';
|
|
11
13
|
import { defaultControlPlaneDir } from '../core/config.js';
|
|
12
14
|
import { verifyIntegrityManifest } from '../core/integrity.js';
|
|
13
|
-
import { diagnoseJudge } from '../core/judge-doctor.js';
|
|
15
|
+
import { diagnoseJudge, stopJudgeSessionBrokers } from '../core/judge-doctor.js';
|
|
16
|
+
import { resolveJudgeTransport } from '../core/judge-runtime-detection.js';
|
|
14
17
|
import { getManagedHookEntries } from '../defaults.js';
|
|
15
18
|
import { resolveNodeBinary } from '../node-resolution.js';
|
|
16
19
|
import { egressStatus } from '../services/egress-service.js';
|
|
@@ -78,7 +81,9 @@ export async function doctorProject(options = {}) {
|
|
|
78
81
|
if (loadedConfig.version !== 4) {
|
|
79
82
|
warnings.push(`Config version is ${loadedConfig.version}; expected 4. Run belay upgrade to migrate.`);
|
|
80
83
|
}
|
|
81
|
-
const
|
|
84
|
+
const transport = resolveJudgeTransport(loadedConfig.judge);
|
|
85
|
+
const shouldProbe = transport.endsWith('-cli');
|
|
86
|
+
const judgeDoctor = await diagnoseJudge(loadedConfig, repoRoot, { liveProbe: shouldProbe });
|
|
82
87
|
issues.push(...judgeDoctor.issues);
|
|
83
88
|
warnings.push(...judgeDoctor.warnings);
|
|
84
89
|
notes.push(...judgeDoctor.notes);
|
|
@@ -239,6 +244,16 @@ export async function doctorProject(options = {}) {
|
|
|
239
244
|
else {
|
|
240
245
|
notes.push('No orphan approval cleanup actions were needed.');
|
|
241
246
|
}
|
|
247
|
+
const judgeStateDir = belayStateDir(loadedConfig, repoLocalDir);
|
|
248
|
+
if (options.dryRun !== true) {
|
|
249
|
+
const stoppedBrokers = await stopJudgeSessionBrokers(repoRoot, judgeStateDir);
|
|
250
|
+
if (stoppedBrokers > 0) {
|
|
251
|
+
notes.push(`Stopped judge session broker artifacts for ${repoRoot}.`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
notes.push('Dry run: would stop judge session broker and clear kill switch if present.');
|
|
256
|
+
}
|
|
242
257
|
}
|
|
243
258
|
let dogfood = null;
|
|
244
259
|
if (loadedConfig) {
|
|
@@ -284,6 +299,36 @@ export async function doctorProject(options = {}) {
|
|
|
284
299
|
for (const issue of sandbox.issues) {
|
|
285
300
|
warnings.push(issue);
|
|
286
301
|
}
|
|
302
|
+
for (const advisory of sandbox.advisories) {
|
|
303
|
+
warnings.push(advisory);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const boundaryDriver = loadedConfig.capability?.boundaryDriver ??
|
|
307
|
+
(loadedConfig.sandbox.runtime === 'container' ? 'container' : null);
|
|
308
|
+
if (loadedConfig.capability || boundaryDriver === 'container') {
|
|
309
|
+
const attestationPath = boundaryAttestationPath(repoRoot, loadedConfig);
|
|
310
|
+
const attestationFormat = await inspectBoundaryAttestationFile(attestationPath);
|
|
311
|
+
if (attestationFormat === 'legacy') {
|
|
312
|
+
warnings.push('Boundary attestation uses an unsigned legacy format. Run belay session start to re-sign it.');
|
|
313
|
+
}
|
|
314
|
+
else if (attestationFormat === 'invalid') {
|
|
315
|
+
warnings.push('Boundary attestation file is present but invalid. Run belay session start to regenerate it.');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (boundaryDriver === 'container') {
|
|
319
|
+
const session = await boundarySessionStatus({ repoRoot, config: loadedConfig });
|
|
320
|
+
if (!session.attestation) {
|
|
321
|
+
warnings.push('Container boundary driver is configured but no attestation was found. Run belay session start.');
|
|
322
|
+
}
|
|
323
|
+
else if (!session.fresh) {
|
|
324
|
+
warnings.push('Boundary attestation is stale or expired. Run belay session start to refresh.');
|
|
325
|
+
}
|
|
326
|
+
else if (session.attestation.probeSignals.includes('repo-mount-ro-default')) {
|
|
327
|
+
notes.push('Container boundary defaults to read-only directory mounts; predicted writes use read-write mounts for the working directory only.');
|
|
328
|
+
}
|
|
329
|
+
else if (session.attestation.probeSignals.includes('repo-mount-rw')) {
|
|
330
|
+
notes.push('Container boundary mounts the working directory read-write (repo-mount-rw). Policy grants scope widening; paths outside the mount are not container-enforced.');
|
|
331
|
+
}
|
|
287
332
|
}
|
|
288
333
|
if (loadedConfig.egress.enabled) {
|
|
289
334
|
const egress = await egressStatus({ targetDir: repoRoot });
|
package/dist/commands/explain.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { loadApprovalState, loadConfigFile } from '../config-io.js';
|
|
3
3
|
import { compactApprovals } from '../core/approval.js';
|
|
4
|
+
import { extractJudgeFallbackReason, formatJudgeRecoveryHint, inferProviderIdFromFallbackReason, isJudgeInfrastructureFailure, } from '../core/judge-fallback-hints.js';
|
|
4
5
|
import { classifyForReport } from './classify-for-report.js';
|
|
5
6
|
export async function explainCommand(options) {
|
|
6
7
|
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
@@ -11,11 +12,16 @@ export async function explainCommand(options) {
|
|
|
11
12
|
const latest = [...pending.approvals].sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))[0];
|
|
12
13
|
const inputKind = latest.inputKind ??
|
|
13
14
|
(latest.kind === 'egress' || latest.kind === 'capability' ? 'shell' : latest.kind);
|
|
15
|
+
const payload = latest.payloadJson !== undefined
|
|
16
|
+
? JSON.parse(latest.payloadJson)
|
|
17
|
+
: undefined;
|
|
14
18
|
const classified = await classifyForReport({
|
|
15
19
|
targetDir: repoRoot,
|
|
16
|
-
cwd: options.cwd,
|
|
20
|
+
cwd: latest.cwd ?? options.cwd,
|
|
17
21
|
kind: inputKind,
|
|
18
22
|
command: latest.input ?? latest.summary,
|
|
23
|
+
toolName: latest.toolName,
|
|
24
|
+
payload,
|
|
19
25
|
});
|
|
20
26
|
return {
|
|
21
27
|
repoRoot: classified.repoRoot,
|
|
@@ -129,5 +135,14 @@ export function formatExplainReport(report) {
|
|
|
129
135
|
? 'Observed assessment: measured in an isolated git worktree at gate time. Observed-safe commands are applied once and the hook denies re-execution (transactional_already_applied).'
|
|
130
136
|
: 'Observed assessment: not applicable (transactional path not eligible).',
|
|
131
137
|
];
|
|
132
|
-
|
|
138
|
+
const recoveryLines = [];
|
|
139
|
+
if (isJudgeInfrastructureFailure(result)) {
|
|
140
|
+
const fallbackReason = extractJudgeFallbackReason(result);
|
|
141
|
+
const providerId = inferProviderIdFromFallbackReason(fallbackReason);
|
|
142
|
+
const recoveryHint = formatJudgeRecoveryHint(providerId, fallbackReason);
|
|
143
|
+
if (recoveryHint) {
|
|
144
|
+
recoveryLines.push('', 'Recovery:', ` ${recoveryHint}`, ' Then verify: belay judge test --live-probe');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return `${[...lines, ...recoveryLines].join('\n')}\n`;
|
|
133
148
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AuditRecord } from '../core/audit-types.js';
|
|
2
|
+
import { type HarvestReport, type HarvestReviewOutcome } from '../core/harvest.js';
|
|
3
|
+
export interface HarvestListOptions {
|
|
4
|
+
targetDir?: string;
|
|
5
|
+
since?: string;
|
|
6
|
+
until?: string;
|
|
7
|
+
json?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface HarvestApplyOptions {
|
|
10
|
+
targetDir?: string;
|
|
11
|
+
command: string;
|
|
12
|
+
outcome: HarvestReviewOutcome;
|
|
13
|
+
reason?: string;
|
|
14
|
+
corpusPath?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function harvestListProject(options?: HarvestListOptions): Promise<HarvestReport>;
|
|
17
|
+
export declare function harvestReportFromRecords(records: AuditRecord[], options?: {
|
|
18
|
+
since?: string;
|
|
19
|
+
until?: string;
|
|
20
|
+
allowPatterns?: string[];
|
|
21
|
+
}): HarvestReport;
|
|
22
|
+
export declare function formatHarvestReport(report: HarvestReport): string;
|
|
23
|
+
export declare function harvestApplyProject(options: HarvestApplyOptions): Promise<{
|
|
24
|
+
ok: boolean;
|
|
25
|
+
message: string;
|
|
26
|
+
corpusPath: string;
|
|
27
|
+
}>;
|
|
28
|
+
/** Parse audit ndjson for tests without full project layout. */
|
|
29
|
+
export declare function harvestReportFromNdjson(raw: string): HarvestReport;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadConfigFile } from '../config-io.js';
|
|
4
|
+
import { parseAuditNdjson } from '../core/audit-metrics.js';
|
|
5
|
+
import { toAuditRecord } from '../core/audit-query.js';
|
|
6
|
+
import { applyHarvestReview, buildHarvestReport, filterRecordsForHarvest, } from '../core/harvest.js';
|
|
7
|
+
import { parseCorpusCases } from '../corpus/types.js';
|
|
8
|
+
import { loadAuditRecords } from './audit.js';
|
|
9
|
+
export async function harvestListProject(options = {}) {
|
|
10
|
+
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
11
|
+
const config = await loadConfigFile(repoRoot);
|
|
12
|
+
const records = await loadAuditRecords(repoRoot);
|
|
13
|
+
return harvestReportFromRecords(records, {
|
|
14
|
+
since: options.since,
|
|
15
|
+
until: options.until,
|
|
16
|
+
allowPatterns: config.overrides.allow,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export function harvestReportFromRecords(records, options = {}) {
|
|
20
|
+
return buildHarvestReport(filterRecordsForHarvest(records, { since: options.since, until: options.until }), { allowPatterns: options.allowPatterns });
|
|
21
|
+
}
|
|
22
|
+
export function formatHarvestReport(report) {
|
|
23
|
+
const lines = [
|
|
24
|
+
`belay harvest (scope: ${report.scope} audit traces only)`,
|
|
25
|
+
`Schema: v${report.schemaVersion}`,
|
|
26
|
+
'',
|
|
27
|
+
`Benign candidates (${report.candidates.length}):`,
|
|
28
|
+
];
|
|
29
|
+
if (report.candidates.length === 0) {
|
|
30
|
+
lines.push('- (none)');
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
for (const candidate of report.candidates) {
|
|
34
|
+
lines.push(`- ${JSON.stringify(candidate.command)} [${candidate.sources.join(', ')}] asks=${candidate.askCount} approved=${candidate.approvedAfterDeny ? 'yes' : 'no'} fp=${candidate.fingerprint.slice(0, 12)}…`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
lines.push('', `Availability queue (${report.availabilityQueue.length}):`);
|
|
38
|
+
if (report.availabilityQueue.length === 0) {
|
|
39
|
+
lines.push('- (none)');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
for (const entry of report.availabilityQueue) {
|
|
43
|
+
lines.push(`- ${JSON.stringify(entry.command)} signal=${entry.availabilitySignal} asks=${entry.askCount} (${entry.reason})`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
lines.push('', 'Candidates are review-only signals — approve in audit does not auto-promote to corpus.', 'Time filters (--since/--until) keep paired deny/approval rows for round-trip detection.', 'Use: belay harvest apply --command "<text>" --outcome provably-benign|accepted-benign|reject');
|
|
47
|
+
return lines.join('\n');
|
|
48
|
+
}
|
|
49
|
+
export async function harvestApplyProject(options) {
|
|
50
|
+
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
51
|
+
const corpusPath = path.resolve(repoRoot, options.corpusPath ?? path.join('corpus', 'shell-commands.json'));
|
|
52
|
+
const raw = await readFile(corpusPath, 'utf8');
|
|
53
|
+
const cases = parseCorpusCases(JSON.parse(raw));
|
|
54
|
+
const result = applyHarvestReview(cases, {
|
|
55
|
+
command: options.command,
|
|
56
|
+
outcome: options.outcome,
|
|
57
|
+
reason: options.reason,
|
|
58
|
+
});
|
|
59
|
+
if (result.applied) {
|
|
60
|
+
await writeFile(corpusPath, `${JSON.stringify(result.cases, null, 2)}\n`, 'utf8');
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
ok: result.ok,
|
|
64
|
+
message: result.message,
|
|
65
|
+
corpusPath: path.relative(repoRoot, corpusPath) || corpusPath,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Parse audit ndjson for tests without full project layout. */
|
|
69
|
+
export function harvestReportFromNdjson(raw) {
|
|
70
|
+
const records = parseAuditNdjson(raw).map((entry) => toAuditRecord(entry));
|
|
71
|
+
return harvestReportFromRecords(records);
|
|
72
|
+
}
|