@guilz-dev/belay 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -15
- package/dist/adapters/shared/gate-runtime.js +25 -9
- package/dist/bundle/claude-runtime.mjs +1786 -379
- package/dist/bundle/codex-runtime.mjs +1785 -379
- package/dist/bundle/cursor-runtime.mjs +1786 -379
- package/dist/cli.js +201 -11
- package/dist/commands/approve.js +11 -0
- package/dist/commands/classify-for-report.js +3 -3
- package/dist/commands/config.d.ts +70 -0
- package/dist/commands/config.js +494 -0
- package/dist/commands/doctor.js +3 -2
- package/dist/commands/explain.js +14 -14
- package/dist/commands/health-snapshot.d.ts +6 -0
- package/dist/commands/health-snapshot.js +34 -0
- package/dist/commands/judge.d.ts +90 -0
- package/dist/commands/judge.js +285 -0
- package/dist/commands/recover.js +2 -2
- package/dist/commands/status.js +2 -2
- package/dist/commands/stdin-key.d.ts +1 -0
- package/dist/commands/stdin-key.js +8 -0
- package/dist/conformance/guarantee-table.js +12 -0
- package/dist/conformance/types.d.ts +2 -0
- package/dist/core/approval.d.ts +3 -0
- package/dist/core/approval.js +18 -3
- package/dist/core/audit-io.d.ts +2 -0
- package/dist/core/audit-io.js +14 -0
- package/dist/core/audit-query.js +5 -1
- package/dist/core/capability/index.d.ts +2 -2
- package/dist/core/capability/index.js +2 -2
- package/dist/core/capability/paths.d.ts +1 -0
- package/dist/core/capability/paths.js +74 -6
- package/dist/core/capability/reasons.d.ts +3 -0
- package/dist/core/capability/reasons.js +8 -0
- package/dist/core/classify-tool.js +97 -27
- package/dist/core/config-layers.js +2 -1
- package/dist/core/config.d.ts +23 -3
- package/dist/core/config.js +112 -13
- package/dist/core/credential-store.d.ts +11 -0
- package/dist/core/credential-store.js +60 -0
- package/dist/core/gate-contract.d.ts +1 -1
- package/dist/core/gate-contract.js +1 -1
- package/dist/core/gate-engine.js +105 -14
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +2 -2
- package/dist/core/integrity.d.ts +2 -0
- package/dist/core/integrity.js +13 -0
- package/dist/core/judge-api-key.d.ts +19 -0
- package/dist/core/judge-api-key.js +74 -0
- package/dist/core/judge-cloud-consent.d.ts +13 -0
- package/dist/core/judge-cloud-consent.js +38 -0
- package/dist/core/judge-config.d.ts +45 -4
- package/dist/core/judge-config.js +266 -44
- package/dist/core/judge-doctor.d.ts +6 -1
- package/dist/core/judge-doctor.js +148 -97
- package/dist/core/judge-model-discovery.d.ts +24 -0
- package/dist/core/judge-model-discovery.js +168 -0
- package/dist/core/judge-model-policy.d.ts +5 -0
- package/dist/core/judge-model-policy.js +21 -0
- package/dist/core/judge-runtime-detection.d.ts +9 -0
- package/dist/core/judge-runtime-detection.js +68 -0
- package/dist/core/transactional/diff-evaluator.js +1 -19
- package/dist/core/types.d.ts +7 -3
- package/dist/core/{v2 → verdict}/adapter.d.ts +1 -0
- package/dist/core/{v2 → verdict}/adapter.js +16 -4
- package/dist/core/{v2 → verdict}/containment.d.ts +5 -0
- package/dist/core/{v2 → verdict}/containment.js +32 -2
- package/dist/core/{v2 → verdict}/egress-classify.js +3 -0
- package/dist/core/verdict/judge-catalog.d.ts +40 -0
- package/dist/core/verdict/judge-catalog.js +148 -0
- package/dist/core/verdict/judge-cli.d.ts +23 -0
- package/dist/core/verdict/judge-cli.js +280 -0
- package/dist/core/{v2 → verdict}/judge-factory.d.ts +15 -4
- package/dist/core/verdict/judge-factory.js +178 -0
- package/dist/core/{v2 → verdict}/judge.d.ts +20 -1
- package/dist/core/{v2 → verdict}/judge.js +85 -18
- package/dist/core/{v2 → verdict}/launcher-resolve.js +72 -1
- package/dist/core/verdict/persistent-paths.d.ts +8 -0
- package/dist/core/verdict/persistent-paths.js +52 -0
- package/dist/core/{v2 → verdict}/types.d.ts +6 -2
- package/dist/core/{v2 → verdict}/verdict.js +176 -46
- package/dist/corpus/evaluate.js +2 -2
- package/dist/installer.js +66 -14
- package/dist/types.d.ts +8 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
- package/skills/belay/SKILL.md +8 -7
- package/dist/commands/init-wizard.d.ts +0 -16
- package/dist/commands/init-wizard.js +0 -50
- package/dist/core/v2/judge-factory.js +0 -75
- /package/dist/core/{v2 → verdict}/egress-classify.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/fingerprint.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/fingerprint.js +0 -0
- /package/dist/core/{v2 → verdict}/index.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/index.js +0 -0
- /package/dist/core/{v2 → verdict}/judge-audit.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/judge-audit.js +0 -0
- /package/dist/core/{v2 → verdict}/judge-outbound.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/judge-outbound.js +0 -0
- /package/dist/core/{v2 → verdict}/launcher-resolve.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/overrides.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/overrides.js +0 -0
- /package/dist/core/{v2 → verdict}/parser.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/parser.js +0 -0
- /package/dist/core/{v2 → verdict}/types.js +0 -0
- /package/dist/core/{v2 → verdict}/verdict.d.ts +0 -0
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { relativeWithinRepo } from '../path-utils.js';
|
|
1
|
+
import { resolveMutationTarget } from '../path-utils.js';
|
|
3
2
|
import { extractRedirectTargets, tokenizeShell } from '../shell-tokenizer.js';
|
|
4
|
-
import { analyzePathTargets, cwdRelative, resolveTrustedPath } from './containment.js';
|
|
3
|
+
import { analyzePathTargets, cwdRelative, isDestructiveHighStakesMutation, resolveTrustedPath, touchesProtectedRoot, } from './containment.js';
|
|
5
4
|
import { classifyEgressTool } from './egress-classify.js';
|
|
6
5
|
import { verdictFingerprint } from './fingerprint.js';
|
|
7
|
-
import { prescanInterpreterCode, tier1RequiresAsk } from './judge.js';
|
|
6
|
+
import { mutationPrescanRequiresAsk, prescanInterpreterCode, tier1RequiresAsk } from './judge.js';
|
|
8
7
|
import { isRoutineLauncher, resolveLauncherRecipe } from './launcher-resolve.js';
|
|
9
8
|
import { allowFromCustomOverride, askFromCustomExternal, customAllowMatch, customExternalMatch, } from './overrides.js';
|
|
10
9
|
import { extractRecursiveScript, isBareInterpreter, isVariableIndirectHead, parseSegment, peelTransparentWrappers, redactCommand, segmentOpacity, splitTopLevelSegments, substitutionInners, } from './parser.js';
|
|
@@ -91,6 +90,7 @@ const LOCAL_ROUTINE_HEADS = new Set([
|
|
|
91
90
|
'make',
|
|
92
91
|
'cmake',
|
|
93
92
|
]);
|
|
93
|
+
const BELAY_SELF_COMMANDS = new Set(['approve', 'revoke']);
|
|
94
94
|
const FIND_DANGEROUS_FLAGS = new Set(['-delete', '-exec', '-execdir', '-ok', '-okdir']);
|
|
95
95
|
function isFindDangerous(tokens) {
|
|
96
96
|
return tokens.some((token) => FIND_DANGEROUS_FLAGS.has(token) || token.startsWith('-exec') || token.startsWith('-ok'));
|
|
@@ -256,8 +256,45 @@ function tier0ExternalMatch(key, head, tokens) {
|
|
|
256
256
|
}
|
|
257
257
|
return false;
|
|
258
258
|
}
|
|
259
|
-
function
|
|
259
|
+
function isBelayJudgeSelfCommand(tokens) {
|
|
260
|
+
return (tokens[0] === 'belay' &&
|
|
261
|
+
tokens[1] === 'judge' &&
|
|
262
|
+
(tokens[2] === 'use' ||
|
|
263
|
+
tokens[2] === 'status' ||
|
|
264
|
+
tokens[2] === 'list' ||
|
|
265
|
+
tokens[2] === 'test' ||
|
|
266
|
+
tokens[2] === 'consent'));
|
|
267
|
+
}
|
|
268
|
+
function isBelayConfigSelfCommand(tokens) {
|
|
269
|
+
if (tokens[0] !== 'belay' || tokens[1] !== 'config') {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
const sub = tokens[2];
|
|
273
|
+
if (!sub) {
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
if (sub === 'list' || sub === 'get' || sub === 'judge') {
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
if (sub === 'credential') {
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
if (sub === 'set' || sub === 'unset') {
|
|
283
|
+
const pathKey = tokens[3] ?? '';
|
|
284
|
+
return pathKey.startsWith('judge.');
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
function isBelaySelfCommand(tokens) {
|
|
260
289
|
const head = tokens[0] ?? '';
|
|
290
|
+
const subcommand = tokens[1] ?? '';
|
|
291
|
+
return (head === 'belay' &&
|
|
292
|
+
(BELAY_SELF_COMMANDS.has(subcommand) ||
|
|
293
|
+
isBelayJudgeSelfCommand(tokens) ||
|
|
294
|
+
isBelayConfigSelfCommand(tokens)));
|
|
295
|
+
}
|
|
296
|
+
function tier0HighStakesRm(tokens, context) {
|
|
297
|
+
const head = (tokens[0] ?? '').split('/').pop() ?? '';
|
|
261
298
|
if (head !== 'rm') {
|
|
262
299
|
return null;
|
|
263
300
|
}
|
|
@@ -280,31 +317,40 @@ function tier0HighStakesRm(tokens, context) {
|
|
|
280
317
|
signals: ['missing_trusted_cwd', ...analysis.signals],
|
|
281
318
|
});
|
|
282
319
|
}
|
|
283
|
-
if (analysis.isHighStakes) {
|
|
284
|
-
return askVerdict({
|
|
285
|
-
location: analysis.location,
|
|
286
|
-
opacity: 'transparent',
|
|
287
|
-
effect: 'local_mutation',
|
|
288
|
-
confidence: 'deterministic',
|
|
289
|
-
reason: 'high_stakes_path',
|
|
290
|
-
signals: ['high_stakes_path', ...analysis.signals],
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
320
|
for (const target of targets) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
321
|
+
const normalized = target.replace(/^['"]|['"]$/g, '');
|
|
322
|
+
if (normalized === '~' || normalized === '$HOME' || normalized.startsWith('~/')) {
|
|
323
|
+
return askVerdict({
|
|
324
|
+
location: 'repo_outside',
|
|
325
|
+
opacity: 'transparent',
|
|
326
|
+
effect: 'local_mutation',
|
|
327
|
+
confidence: 'deterministic',
|
|
328
|
+
reason: 'high_stakes_path',
|
|
329
|
+
signals: ['catastrophic_home_deletion', ...analysis.signals],
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (analysis.isHighStakes && analysis.signals.includes('high_stakes_path')) {
|
|
334
|
+
let destructiveHighStakes = false;
|
|
335
|
+
for (const target of targets) {
|
|
336
|
+
const resolved = resolveTrustedPath(target, context.cwd, context.trustedCwd) ??
|
|
337
|
+
resolveMutationTarget(target, context.cwd);
|
|
338
|
+
if (resolved &&
|
|
339
|
+
isDestructiveHighStakesMutation(head, resolved, context.repoRoot, context.sensitivePaths, context.protectedArtifactRoots)) {
|
|
340
|
+
destructiveHighStakes = true;
|
|
341
|
+
break;
|
|
306
342
|
}
|
|
307
343
|
}
|
|
344
|
+
if (destructiveHighStakes) {
|
|
345
|
+
return askVerdict({
|
|
346
|
+
location: analysis.location,
|
|
347
|
+
opacity: 'transparent',
|
|
348
|
+
effect: 'local_mutation',
|
|
349
|
+
confidence: 'deterministic',
|
|
350
|
+
reason: 'high_stakes_path',
|
|
351
|
+
signals: ['high_stakes_path', ...analysis.signals],
|
|
352
|
+
});
|
|
353
|
+
}
|
|
308
354
|
}
|
|
309
355
|
return null;
|
|
310
356
|
}
|
|
@@ -488,6 +534,16 @@ async function evaluateSegment(command, context, depth) {
|
|
|
488
534
|
if (rmVerdict) {
|
|
489
535
|
return rmVerdict;
|
|
490
536
|
}
|
|
537
|
+
if (isBelaySelfCommand(peeled)) {
|
|
538
|
+
return allowVerdict({
|
|
539
|
+
location: 'unknown',
|
|
540
|
+
opacity: 'transparent',
|
|
541
|
+
effect: 'local_mutation',
|
|
542
|
+
confidence: 'deterministic',
|
|
543
|
+
reason: 'belay_control_plane_command',
|
|
544
|
+
signals: ['belay_control_plane_command', segment.head],
|
|
545
|
+
});
|
|
546
|
+
}
|
|
491
547
|
let effect = 'unknown';
|
|
492
548
|
if (READ_ONLY_KEYS.has(segment.key) || READ_ONLY_KEYS.has(segment.head)) {
|
|
493
549
|
effect = 'read_only';
|
|
@@ -499,6 +555,9 @@ async function evaluateSegment(command, context, depth) {
|
|
|
499
555
|
effect = 'local_mutation';
|
|
500
556
|
}
|
|
501
557
|
const pathArgs = extractPathArgs(peeled);
|
|
558
|
+
if (extractRedirectTargets(peeled).length > 0 && effect === 'read_only') {
|
|
559
|
+
effect = 'local_mutation';
|
|
560
|
+
}
|
|
502
561
|
const pathAnalysis = analyzePathTargets({
|
|
503
562
|
targets: pathArgs,
|
|
504
563
|
cwd: context.cwd,
|
|
@@ -529,15 +588,45 @@ async function evaluateSegment(command, context, depth) {
|
|
|
529
588
|
});
|
|
530
589
|
}
|
|
531
590
|
}
|
|
532
|
-
if (
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
591
|
+
if ((effect === 'local_mutation' || effect === 'unknown') &&
|
|
592
|
+
context.protectedArtifactRoots &&
|
|
593
|
+
context.protectedArtifactRoots.length > 0) {
|
|
594
|
+
for (const target of pathArgs) {
|
|
595
|
+
const resolved = resolveTrustedPath(target, context.cwd, context.trustedCwd) ??
|
|
596
|
+
resolveMutationTarget(target, context.cwd);
|
|
597
|
+
if (resolved && touchesProtectedRoot(resolved, context.protectedArtifactRoots)) {
|
|
598
|
+
return askVerdict({
|
|
599
|
+
location: pathAnalysis.location,
|
|
600
|
+
opacity: 'transparent',
|
|
601
|
+
effect: 'local_mutation',
|
|
602
|
+
confidence: 'deterministic',
|
|
603
|
+
reason: 'high_stakes_path',
|
|
604
|
+
signals: ['high_stakes_path', 'control_plane_path'],
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (effect === 'local_mutation' || effect === 'unknown') {
|
|
610
|
+
let destructiveHighStakes = false;
|
|
611
|
+
for (const target of pathArgs) {
|
|
612
|
+
const resolved = resolveTrustedPath(target, context.cwd, context.trustedCwd) ??
|
|
613
|
+
resolveMutationTarget(target, context.cwd);
|
|
614
|
+
if (resolved &&
|
|
615
|
+
isDestructiveHighStakesMutation(segment.head, resolved, context.repoRoot, context.sensitivePaths, context.protectedArtifactRoots)) {
|
|
616
|
+
destructiveHighStakes = true;
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (destructiveHighStakes) {
|
|
621
|
+
return askVerdict({
|
|
622
|
+
location: pathAnalysis.location,
|
|
623
|
+
opacity: 'transparent',
|
|
624
|
+
effect: 'local_mutation',
|
|
625
|
+
confidence: 'deterministic',
|
|
626
|
+
reason: 'high_stakes_path',
|
|
627
|
+
signals: pathAnalysis.signals,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
541
630
|
}
|
|
542
631
|
if (segment.head === 'find' && isFindDangerous(peeled)) {
|
|
543
632
|
return askVerdict({
|
|
@@ -549,15 +638,15 @@ async function evaluateSegment(command, context, depth) {
|
|
|
549
638
|
signals: ['find_dangerous_action'],
|
|
550
639
|
});
|
|
551
640
|
}
|
|
552
|
-
if (
|
|
553
|
-
|
|
554
|
-
return
|
|
641
|
+
if (effect === 'read_only' &&
|
|
642
|
+
(pathAnalysis.location === 'repo_outside' || pathAnalysis.location === 'mixed')) {
|
|
643
|
+
return allowVerdict({
|
|
555
644
|
location: pathAnalysis.location,
|
|
556
|
-
opacity
|
|
557
|
-
effect:
|
|
645
|
+
opacity,
|
|
646
|
+
effect: 'read_only',
|
|
558
647
|
confidence: 'deterministic',
|
|
559
|
-
reason: '
|
|
560
|
-
signals: ['
|
|
648
|
+
reason: 'read_only',
|
|
649
|
+
signals: ['read_only', ...pathAnalysis.signals],
|
|
561
650
|
});
|
|
562
651
|
}
|
|
563
652
|
if (pathAnalysis.location === 'unknown' &&
|
|
@@ -572,7 +661,34 @@ async function evaluateSegment(command, context, depth) {
|
|
|
572
661
|
signals: ['unknown_location_mutation'],
|
|
573
662
|
});
|
|
574
663
|
}
|
|
575
|
-
const
|
|
664
|
+
const outsideMutation = pathAnalysis.location === 'repo_outside' || pathAnalysis.location === 'mixed';
|
|
665
|
+
const mutationPrescan = (effect === 'local_mutation' || effect === 'unknown') && pathArgs.length > 0
|
|
666
|
+
? mutationPrescanRequiresAsk({
|
|
667
|
+
targets: pathArgs,
|
|
668
|
+
cwd: context.cwd,
|
|
669
|
+
repoRoot: context.repoRoot,
|
|
670
|
+
trustedCwd: context.trustedCwd,
|
|
671
|
+
sensitivePaths: context.sensitivePaths,
|
|
672
|
+
})
|
|
673
|
+
: null;
|
|
674
|
+
if (mutationPrescan) {
|
|
675
|
+
return askVerdict({
|
|
676
|
+
location: pathAnalysis.location === 'unknown'
|
|
677
|
+
? 'unknown'
|
|
678
|
+
: pathAnalysis.location === 'repo_outside' || pathAnalysis.location === 'mixed'
|
|
679
|
+
? pathAnalysis.location
|
|
680
|
+
: 'repo_local',
|
|
681
|
+
opacity,
|
|
682
|
+
effect: 'local_mutation',
|
|
683
|
+
confidence: 'deterministic',
|
|
684
|
+
reason: 'tier1_catastrophic',
|
|
685
|
+
signals: ['tier1_catastrophic', mutationPrescan.reason],
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
const needsTier1 = effect === 'unknown' ||
|
|
689
|
+
TIER0_EXTERNAL_HEADS.has(segment.head) ||
|
|
690
|
+
egressClass === 'ambiguous' ||
|
|
691
|
+
(outsideMutation && effect !== 'read_only');
|
|
576
692
|
let tier1Trace;
|
|
577
693
|
if (needsTier1) {
|
|
578
694
|
const tier1Text = recursiveScript ?? command;
|
|
@@ -584,9 +700,13 @@ async function evaluateSegment(command, context, depth) {
|
|
|
584
700
|
tier1Trace = context.judge.lastTrace;
|
|
585
701
|
if (tier1RequiresAsk(tier1)) {
|
|
586
702
|
return askVerdict({
|
|
587
|
-
location: pathAnalysis.location === 'unknown'
|
|
703
|
+
location: pathAnalysis.location === 'unknown'
|
|
704
|
+
? 'unknown'
|
|
705
|
+
: pathAnalysis.location === 'repo_outside' || pathAnalysis.location === 'mixed'
|
|
706
|
+
? pathAnalysis.location
|
|
707
|
+
: 'repo_local',
|
|
588
708
|
opacity,
|
|
589
|
-
effect: tier1.
|
|
709
|
+
effect: !tier1.local_recoverable ? 'remote_mutation' : effect,
|
|
590
710
|
confidence: 'llm',
|
|
591
711
|
reason: 'tier1_catastrophic',
|
|
592
712
|
signals: ['tier1_catastrophic', tier1.reason],
|
|
@@ -594,6 +714,16 @@ async function evaluateSegment(command, context, depth) {
|
|
|
594
714
|
});
|
|
595
715
|
}
|
|
596
716
|
}
|
|
717
|
+
if (outsideMutation && effect !== 'read_only') {
|
|
718
|
+
return withJudgeTrace(allowVerdict({
|
|
719
|
+
location: pathAnalysis.location,
|
|
720
|
+
opacity,
|
|
721
|
+
effect: 'local_mutation',
|
|
722
|
+
confidence: tier1Trace ? 'llm' : 'deterministic',
|
|
723
|
+
reason: 'repo_outside_local_mutation',
|
|
724
|
+
signals: ['repo_outside_local_mutation', ...pathAnalysis.signals],
|
|
725
|
+
}), tier1Trace);
|
|
726
|
+
}
|
|
597
727
|
if (pathAnalysis.location === 'repo_local' &&
|
|
598
728
|
(effect === 'read_only' || effect === 'local_mutation') &&
|
|
599
729
|
opacity !== 'opaque') {
|
package/dist/corpus/evaluate.js
CHANGED
|
@@ -2,8 +2,8 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { classifierOptionsFromConfig, DEFAULT_CONFIG_V3 } from '../core/config.js';
|
|
5
|
-
import { classifyShell } from '../core/
|
|
6
|
-
import { createDeterministicJudgeStub } from '../core/
|
|
5
|
+
import { classifyShell } from '../core/verdict/adapter.js';
|
|
6
|
+
import { createDeterministicJudgeStub } from '../core/verdict/judge.js';
|
|
7
7
|
export function assessmentsDiverge(predicted, observed) {
|
|
8
8
|
return (predicted.reversibility !== observed.reversibility ||
|
|
9
9
|
predicted.external !== observed.external ||
|
package/dist/installer.js
CHANGED
|
@@ -7,9 +7,10 @@ import { resolveScopedPaths } from './adapters/layouts/scope.js';
|
|
|
7
7
|
import { getAdapter } from './adapters/registry.js';
|
|
8
8
|
import { dogfoodProject } from './commands/dogfood.js';
|
|
9
9
|
import { detectAdapterName, loadConfigFile, mergeAndWriteConfig, writeConfigFile, } from './config-io.js';
|
|
10
|
-
import {
|
|
10
|
+
import { appendCliAuditEvent } from './core/audit-io.js';
|
|
11
|
+
import { isFreshConfigInput, mergeConfig, normalizeConfig, } from './core/config.js';
|
|
11
12
|
import { runtimeIntegrityFiles, writeIntegrityManifest } from './core/integrity.js';
|
|
12
|
-
import { resolveInitJudgeConfig } from './core/judge-config.js';
|
|
13
|
+
import { hasValidCloudConsent, isCloudJudgeConfig, migrateImplicitLocalJudgeIfNeeded, resolveInitJudgeConfig, } from './core/judge-config.js';
|
|
13
14
|
import { bootstrapStateFiles, writeSkillArtifacts } from './installer/bootstrap.js';
|
|
14
15
|
import { writeRuntimeArtifacts } from './installer/runtime-artifacts.js';
|
|
15
16
|
import { applyInstallScope, resolveOperationScope } from './installer/scope-config.js';
|
|
@@ -104,31 +105,62 @@ function resolveAdapterName(options, repoRoot) {
|
|
|
104
105
|
}
|
|
105
106
|
return 'cursor';
|
|
106
107
|
}
|
|
107
|
-
async function
|
|
108
|
-
if (
|
|
108
|
+
async function auditJudgeMigrationIfNeeded(repoRoot, adapterName, before, after, by) {
|
|
109
|
+
if (before.providerId === after.providerId && before.provider === after.provider) {
|
|
109
110
|
return;
|
|
110
111
|
}
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
const config = await loadConfigFile(repoRoot, adapterName);
|
|
113
|
+
await appendCliAuditEvent(repoRoot, config, {
|
|
114
|
+
event: 'judge_provider_changed',
|
|
115
|
+
from: { providerId: before.providerId, provider: before.provider },
|
|
116
|
+
to: { providerId: after.providerId, provider: after.provider },
|
|
117
|
+
by,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async function applyInitJudgeConfig(repoRoot, adapterName, options, isFreshBeforeInstall) {
|
|
121
|
+
if (options.skipJudgeWrite) {
|
|
122
|
+
return;
|
|
116
123
|
}
|
|
117
|
-
const isFresh = isFreshConfigInput(existingConfig);
|
|
118
124
|
const mergedConfig = await loadConfigFile(repoRoot, adapterName);
|
|
119
|
-
const hasExplicitJudgeFlags = options.judgeProfile ||
|
|
125
|
+
const hasExplicitJudgeFlags = options.judgeProfile ||
|
|
126
|
+
options.judgeProvider ||
|
|
127
|
+
options.judgeProviderId ||
|
|
128
|
+
options.judgeModel ||
|
|
129
|
+
options.judgeEndpoint;
|
|
120
130
|
const judge = resolveInitJudgeConfig({
|
|
121
|
-
isFresh,
|
|
131
|
+
isFresh: isFreshBeforeInstall,
|
|
122
132
|
hasExplicitJudgeFlags: Boolean(hasExplicitJudgeFlags),
|
|
123
133
|
judgeProfile: options.judgeProfile,
|
|
124
134
|
judgeProvider: options.judgeProvider,
|
|
135
|
+
judgeProviderId: options.judgeProviderId,
|
|
125
136
|
judgeModel: options.judgeModel,
|
|
126
137
|
judgeEndpoint: options.judgeEndpoint,
|
|
127
138
|
acceptCloudJudge: options.acceptCloudJudge,
|
|
139
|
+
interactiveConsent: Boolean(options.acceptCloudJudge && process.stdin.isTTY),
|
|
140
|
+
cloudConsentApprovalId: options.cloudConsentApprovalId,
|
|
128
141
|
existingJudge: mergedConfig.judge,
|
|
142
|
+
adapter: adapterName,
|
|
129
143
|
});
|
|
130
|
-
const
|
|
144
|
+
const migrated = options.migrateJudgeDefault === true
|
|
145
|
+
? migrateImplicitLocalJudgeIfNeeded(mergedConfig.judge, adapterName)
|
|
146
|
+
: null;
|
|
147
|
+
let finalJudge = migrated ?? judge;
|
|
148
|
+
if (options.judgeCredentialMode) {
|
|
149
|
+
finalJudge = {
|
|
150
|
+
...finalJudge,
|
|
151
|
+
credential: options.judgeCredentialMode === 'apiKey'
|
|
152
|
+
? { mode: 'apiKey', ref: 'store:judge' }
|
|
153
|
+
: { mode: 'project' },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const configWithJudge = normalizeConfig({ ...mergedConfig, version: 4, judge: finalJudge });
|
|
157
|
+
if (isCloudJudgeConfig(configWithJudge.judge) && !hasValidCloudConsent(configWithJudge.judge)) {
|
|
158
|
+
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');
|
|
159
|
+
}
|
|
131
160
|
await writeConfigFile(repoRoot, configWithJudge, adapterName);
|
|
161
|
+
if (migrated) {
|
|
162
|
+
await auditJudgeMigrationIfNeeded(repoRoot, adapterName, mergedConfig.judge, finalJudge, options.migrateJudgeDefault ? 'belay init --migrate-judge-default' : 'belay init');
|
|
163
|
+
}
|
|
132
164
|
}
|
|
133
165
|
async function refreshIntegrityManifest(repoRoot, adapterName) {
|
|
134
166
|
const layout = getAdapter(adapterName).layout;
|
|
@@ -141,8 +173,19 @@ export async function initProject(options = {}) {
|
|
|
141
173
|
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
142
174
|
const adapterName = resolveAdapterName(options, repoRoot);
|
|
143
175
|
const adapter = getAdapter(adapterName);
|
|
176
|
+
const configPath = adapter.layout.configPath(repoRoot);
|
|
177
|
+
let isFreshBeforeInstall = true;
|
|
178
|
+
if (await pathExists(configPath)) {
|
|
179
|
+
try {
|
|
180
|
+
const raw = await readFile(configPath, 'utf8');
|
|
181
|
+
isFreshBeforeInstall = isFreshConfigInput(JSON.parse(raw));
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
isFreshBeforeInstall = false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
144
187
|
const result = await adapter.install(repoRoot, options);
|
|
145
|
-
await applyInitJudgeConfig(repoRoot, adapterName, options);
|
|
188
|
+
await applyInitJudgeConfig(repoRoot, adapterName, options, isFreshBeforeInstall);
|
|
146
189
|
if (options.preset) {
|
|
147
190
|
const existing = await loadConfigFile(repoRoot, adapterName);
|
|
148
191
|
const presetConfig = mergeConfig(applyConfigPreset(options.preset));
|
|
@@ -164,6 +207,15 @@ export async function upgradeProject(options = {}) {
|
|
|
164
207
|
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
165
208
|
const adapterName = resolveAdapterName(options, repoRoot);
|
|
166
209
|
await getAdapter(adapterName).upgrade(repoRoot, options);
|
|
210
|
+
if (options.migrateJudgeDefault === true) {
|
|
211
|
+
const mergedConfig = await loadConfigFile(repoRoot, adapterName);
|
|
212
|
+
const migrated = migrateImplicitLocalJudgeIfNeeded(mergedConfig.judge, adapterName);
|
|
213
|
+
if (migrated) {
|
|
214
|
+
const configWithJudge = normalizeConfig({ ...mergedConfig, version: 4, judge: migrated });
|
|
215
|
+
await writeConfigFile(repoRoot, configWithJudge, adapterName);
|
|
216
|
+
await auditJudgeMigrationIfNeeded(repoRoot, adapterName, mergedConfig.judge, migrated, 'belay upgrade --migrate-judge-default');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
167
219
|
await refreshIntegrityManifest(repoRoot, adapterName);
|
|
168
220
|
return { repoRoot, adapter: adapterName };
|
|
169
221
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -20,20 +20,27 @@ export interface InitOptions {
|
|
|
20
20
|
adapter?: AdapterName;
|
|
21
21
|
scope?: InstallScope;
|
|
22
22
|
preset?: import('./presets.js').ConfigPresetName;
|
|
23
|
-
judgeProfile?: 'local-ollama';
|
|
23
|
+
judgeProfile?: 'local-ollama' | 'cursor' | 'claude' | 'codex';
|
|
24
24
|
judgeProvider?: 'ollama' | 'openai-compatible' | 'cursor';
|
|
25
|
+
judgeProviderId?: 'ollama' | 'codex' | 'claude' | 'cursor' | 'local' | 'openai';
|
|
25
26
|
judgeModel?: string;
|
|
26
27
|
judgeEndpoint?: string;
|
|
28
|
+
judgeCredentialMode?: 'project' | 'apiKey';
|
|
27
29
|
/** Acknowledge cloud judge egress + redaction limits (R19). Required for openai-compatible provider. */
|
|
28
30
|
acceptCloudJudge?: boolean;
|
|
31
|
+
cloudConsentApprovalId?: string;
|
|
29
32
|
/** When true, skip writing judge config (used when user declines cloud consent). */
|
|
30
33
|
skipJudgeWrite?: boolean;
|
|
34
|
+
/** Opt-in: migrate implicit factory-default ollama judge to host default provider. */
|
|
35
|
+
migrateJudgeDefault?: boolean;
|
|
31
36
|
}
|
|
32
37
|
export interface UpgradeOptions {
|
|
33
38
|
targetDir?: string;
|
|
34
39
|
withSkill?: boolean;
|
|
35
40
|
adapter?: AdapterName;
|
|
36
41
|
scope?: InstallScope;
|
|
42
|
+
/** Opt-in: migrate implicit factory-default ollama judge to host default provider. */
|
|
43
|
+
migrateJudgeDefault?: boolean;
|
|
37
44
|
}
|
|
38
45
|
export interface DoctorOptions {
|
|
39
46
|
targetDir?: string;
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const PACKAGE_VERSION = "0.
|
|
1
|
+
export declare const PACKAGE_VERSION = "0.3.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/sync-version.mjs — do not edit.
|
|
2
|
-
export const PACKAGE_VERSION = '0.
|
|
2
|
+
export const PACKAGE_VERSION = '0.3.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guilz-dev/belay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Belay-style approval and audit gating for agent runtimes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"lint": "biome check src package.json README.md CHANGELOG.md CONTRIBUTING.md SECURITY.md tsconfig.json tsconfig.build.json vitest.config.ts scripts docs",
|
|
55
55
|
"typecheck": "tsc --noEmit",
|
|
56
56
|
"test": "pnpm build && vitest run",
|
|
57
|
+
"test:structural": "pnpm build && vitest run src/__tests__/verdict/structural-suite.test.ts",
|
|
57
58
|
"test:stable": "pnpm build && vitest run && vitest run && vitest run",
|
|
58
59
|
"corpus": "pnpm build && node scripts/corpus.mjs"
|
|
59
60
|
},
|
package/skills/belay/SKILL.md
CHANGED
|
@@ -29,10 +29,10 @@ you to the CLI. It does not classify commands itself.
|
|
|
29
29
|
|
|
30
30
|
## Prerequisites
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
check whether hooks are present.
|
|
32
|
+
`npx skills add` installs this skill only — the runtime gate is **not installed**
|
|
33
|
+
until hooks are configured. Run `belay config` in the project root for interactive
|
|
34
|
+
setup (or `npx @guilz-dev/belay init` for non-interactive install). Run
|
|
35
|
+
`belay doctor` to check whether hooks are present.
|
|
36
36
|
|
|
37
37
|
## When belay blocks an action
|
|
38
38
|
|
|
@@ -48,16 +48,17 @@ For recovery advice after a block, use `/belay recover` or `belay recover`.
|
|
|
48
48
|
|
|
49
49
|
## Install or repair
|
|
50
50
|
|
|
51
|
-
-
|
|
52
|
-
-
|
|
51
|
+
- Interactive setup: `belay config`
|
|
52
|
+
- Non-interactive install: `npx @guilz-dev/belay init --with-skill`
|
|
53
53
|
- Health check: `belay doctor`
|
|
54
54
|
|
|
55
|
-
Do not run init or doctor implicitly from this skill — only when the user asks.
|
|
55
|
+
Do not run init, config, or doctor implicitly from this skill — only when the user asks.
|
|
56
56
|
|
|
57
57
|
## CLI mapping
|
|
58
58
|
|
|
59
59
|
| User intent | Command |
|
|
60
60
|
| --- | --- |
|
|
61
|
+
| Set up or change judge settings | `belay config` |
|
|
61
62
|
| Why was this blocked? | `belay explain --command "..."` |
|
|
62
63
|
| Explain latest pending ask | `belay explain` |
|
|
63
64
|
| Status / dogfood / audit visibility | `belay status` |
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { AdapterName, InitOptions } from '../types.js';
|
|
2
|
-
export interface WizardAnswers {
|
|
3
|
-
adapter: AdapterName;
|
|
4
|
-
scope: 'project' | 'global';
|
|
5
|
-
withSkill: boolean;
|
|
6
|
-
dogfood: boolean;
|
|
7
|
-
}
|
|
8
|
-
export declare function buildInitOptionsFromWizard(answers: WizardAnswers, targetDir?: string): InitOptions;
|
|
9
|
-
export declare function runInitWizard(options?: {
|
|
10
|
-
targetDir?: string;
|
|
11
|
-
}): Promise<{
|
|
12
|
-
repoRoot: string;
|
|
13
|
-
withSkill: boolean;
|
|
14
|
-
dogfood: boolean;
|
|
15
|
-
adapter: import("../adapters/layouts/types.js").AdapterName;
|
|
16
|
-
}>;
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { stdin as input, stdout as output } from 'node:process';
|
|
2
|
-
import readline from 'node:readline/promises';
|
|
3
|
-
import { initProject } from '../installer.js';
|
|
4
|
-
function parseAdapter(value) {
|
|
5
|
-
const normalized = (value ?? 'cursor').trim().toLowerCase();
|
|
6
|
-
if (normalized === 'claude' || normalized === 'codex' || normalized === 'cursor') {
|
|
7
|
-
return normalized;
|
|
8
|
-
}
|
|
9
|
-
throw new Error(`Unknown adapter: ${value ?? '(empty)'}`);
|
|
10
|
-
}
|
|
11
|
-
function parseScope(value) {
|
|
12
|
-
const normalized = (value ?? 'project').trim().toLowerCase();
|
|
13
|
-
if (normalized === 'global' || normalized === 'project') {
|
|
14
|
-
return normalized;
|
|
15
|
-
}
|
|
16
|
-
throw new Error(`Unknown scope: ${value ?? '(empty)'}`);
|
|
17
|
-
}
|
|
18
|
-
function parseYesNo(value, defaultValue) {
|
|
19
|
-
const normalized = (value ?? (defaultValue ? 'y' : 'n')).trim().toLowerCase();
|
|
20
|
-
if (['y', 'yes', 'true', '1'].includes(normalized)) {
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
23
|
-
if (['n', 'no', 'false', '0'].includes(normalized)) {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
return defaultValue;
|
|
27
|
-
}
|
|
28
|
-
export function buildInitOptionsFromWizard(answers, targetDir) {
|
|
29
|
-
return {
|
|
30
|
-
targetDir,
|
|
31
|
-
adapter: answers.adapter,
|
|
32
|
-
scope: answers.scope,
|
|
33
|
-
withSkill: answers.withSkill,
|
|
34
|
-
dogfood: answers.dogfood,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
export async function runInitWizard(options = {}) {
|
|
38
|
-
const rl = readline.createInterface({ input, output });
|
|
39
|
-
try {
|
|
40
|
-
output.write('belay init wizard\n');
|
|
41
|
-
const adapter = parseAdapter(await rl.question('Adapter (cursor/claude/codex) [cursor]: '));
|
|
42
|
-
const scope = parseScope(await rl.question('Install scope (project/global) [project]: '));
|
|
43
|
-
const withSkill = parseYesNo(await rl.question('Install SKILL.md and slash commands? (y/n) [y]: '), true);
|
|
44
|
-
const dogfood = parseYesNo(await rl.question('Start in audit dogfood mode? (y/n) [n]: '), false);
|
|
45
|
-
return initProject(buildInitOptionsFromWizard({ adapter, scope, withSkill, dogfood }, options.targetDir));
|
|
46
|
-
}
|
|
47
|
-
finally {
|
|
48
|
-
rl.close();
|
|
49
|
-
}
|
|
50
|
-
}
|