@guilz-dev/belay 0.3.0 → 0.4.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/dist/adapters/cursor/runtime-entry.js +1 -0
- package/dist/adapters/shared/gate-runtime.d.ts +11 -14
- package/dist/adapters/shared/gate-runtime.js +277 -19
- package/dist/bundle/claude-runtime.mjs +3138 -699
- package/dist/bundle/codex-runtime.mjs +3162 -711
- package/dist/bundle/cursor-runtime.mjs +3140 -701
- package/dist/cli.js +137 -5
- package/dist/commands/approve.d.ts +1 -0
- package/dist/commands/approve.js +45 -26
- package/dist/commands/config.js +135 -52
- package/dist/commands/doctor.js +11 -1
- package/dist/commands/explain.js +6 -1
- 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 +21 -2
- 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/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 +10 -5
- 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 +47 -2
- package/dist/core/approval.d.ts +24 -3
- package/dist/core/approval.js +47 -4
- 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/index.d.ts +2 -1
- package/dist/core/capability/index.js +1 -0
- package/dist/core/capability/paths.d.ts +2 -2
- package/dist/core/capability/paths.js +9 -9
- 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.js +2 -20
- package/dist/core/classify-tool.js +32 -3
- package/dist/core/config.d.ts +18 -0
- package/dist/core/config.js +49 -0
- package/dist/core/gate-engine.js +6 -6
- 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 +54 -0
- package/dist/core/path-utils.d.ts +11 -0
- package/dist/core/path-utils.js +56 -6
- package/dist/core/reclassify.d.ts +3 -0
- package/dist/core/reclassify.js +44 -14
- 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 +175 -0
- package/dist/core/types.d.ts +15 -0
- package/dist/core/verdict/adapter.js +10 -1
- package/dist/core/verdict/containment.d.ts +4 -3
- package/dist/core/verdict/containment.js +17 -12
- package/dist/core/verdict/judge-audit.d.ts +1 -0
- package/dist/core/verdict/judge-audit.js +32 -1
- package/dist/core/verdict/judge-baseline.d.ts +20 -0
- package/dist/core/verdict/judge-baseline.js +48 -0
- package/dist/core/verdict/judge-broker-service.d.ts +42 -0
- package/dist/core/verdict/judge-broker-service.js +279 -0
- package/dist/core/verdict/judge-cli-fingerprint.d.ts +5 -0
- package/dist/core/verdict/judge-cli-fingerprint.js +61 -0
- package/dist/core/verdict/judge-cli.d.ts +23 -0
- package/dist/core/verdict/judge-cli.js +137 -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 +42 -0
- package/dist/core/verdict/judge-runtime-config.js +92 -0
- package/dist/core/verdict/judge-session-broker.d.ts +48 -0
- package/dist/core/verdict/judge-session-broker.js +195 -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 +40 -0
- package/dist/core/verdict/judge-transport.js +258 -0
- package/dist/core/verdict/judge.d.ts +14 -1
- package/dist/core/verdict/judge.js +6 -5
- package/dist/core/verdict/parser.js +6 -1
- package/dist/core/verdict/types.d.ts +13 -0
- package/dist/core/verdict/verdict.js +7 -4
- 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 +62 -5
- 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 +151 -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 +45 -0
- package/dist/corpus/standing-allow-catalog.generated.d.ts +13 -0
- package/dist/corpus/standing-allow-catalog.generated.js +79 -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 +123 -0
- package/dist/services/sandbox-service.d.ts +1 -0
- package/dist/services/sandbox-service.js +2 -0
- 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/cli.js
CHANGED
|
@@ -6,11 +6,14 @@ import { auditProject, formatAuditReport } from './commands/audit.js';
|
|
|
6
6
|
import { doctorProject, formatDoctorReport } from './commands/doctor.js';
|
|
7
7
|
import { dogfoodProject, formatDogfoodResult } from './commands/dogfood.js';
|
|
8
8
|
import { explainCommand, formatExplainReport } from './commands/explain.js';
|
|
9
|
+
import { formatHarvestReport, harvestApplyProject, harvestListProject } from './commands/harvest.js';
|
|
9
10
|
import { formatMetricsReport, metricsProject } from './commands/metrics.js';
|
|
11
|
+
import { formatQualityReport, qualityCheck } from './commands/quality.js';
|
|
10
12
|
import { formatRecoverReport, recoverProject } from './commands/recover.js';
|
|
11
13
|
import { formatReport, reportProject } from './commands/report.js';
|
|
12
14
|
import { revokeApproval } from './commands/revoke.js';
|
|
13
15
|
import { formatSimulateReport, simulateProject } from './commands/simulate.js';
|
|
16
|
+
import { revokeStandingAllow } from './commands/standing-allow.js';
|
|
14
17
|
import { formatStatusReport, statusProject } from './commands/status.js';
|
|
15
18
|
import { loadConfigFile } from './config-io.js';
|
|
16
19
|
import { rejectDeprecatedJudgeModelAuto } from './core/judge-model-policy.js';
|
|
@@ -154,6 +157,13 @@ function parseArgs(argv) {
|
|
|
154
157
|
index += 1;
|
|
155
158
|
continue;
|
|
156
159
|
}
|
|
160
|
+
if (token === '--live-probe') {
|
|
161
|
+
if (command !== 'judge') {
|
|
162
|
+
throw new Error('--live-probe is only valid for judge test.');
|
|
163
|
+
}
|
|
164
|
+
options.judgeLiveProbe = true;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
157
167
|
if (token === '--json') {
|
|
158
168
|
options.json = true;
|
|
159
169
|
continue;
|
|
@@ -178,6 +188,24 @@ function parseArgs(argv) {
|
|
|
178
188
|
index += 1;
|
|
179
189
|
continue;
|
|
180
190
|
}
|
|
191
|
+
if (token === '--outcome') {
|
|
192
|
+
const next = rest[index + 1];
|
|
193
|
+
if (!next || !['provably-benign', 'accepted-benign', 'reject'].includes(next)) {
|
|
194
|
+
throw new Error('--outcome requires provably-benign, accepted-benign, or reject.');
|
|
195
|
+
}
|
|
196
|
+
options.harvestOutcome = next;
|
|
197
|
+
index += 1;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (token === '--corpus') {
|
|
201
|
+
const next = rest[index + 1];
|
|
202
|
+
if (!next) {
|
|
203
|
+
throw new Error('--corpus requires a path.');
|
|
204
|
+
}
|
|
205
|
+
options.corpusPath = next;
|
|
206
|
+
index += 1;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
181
209
|
if (token === '--kind') {
|
|
182
210
|
const next = rest[index + 1];
|
|
183
211
|
if (!next) {
|
|
@@ -252,11 +280,18 @@ function parseArgs(argv) {
|
|
|
252
280
|
index += 1;
|
|
253
281
|
continue;
|
|
254
282
|
}
|
|
283
|
+
if (token === '--replay') {
|
|
284
|
+
if (command !== 'approve') {
|
|
285
|
+
throw new Error('--replay is only valid for approve.');
|
|
286
|
+
}
|
|
287
|
+
options.approveReplay = true;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
255
290
|
if (token === '--scope') {
|
|
256
291
|
const next = rest[index + 1];
|
|
257
292
|
if (command === 'approve') {
|
|
258
|
-
if (!next || !['once', 'domain', 'path'].includes(next)) {
|
|
259
|
-
throw new Error('--scope requires once, domain, or
|
|
293
|
+
if (!next || !['once', 'domain', 'path', 'workspace-root'].includes(next)) {
|
|
294
|
+
throw new Error('--scope requires once, domain, path, or workspace-root.');
|
|
260
295
|
}
|
|
261
296
|
options.approveScope = next;
|
|
262
297
|
}
|
|
@@ -315,6 +350,9 @@ function parseArgs(argv) {
|
|
|
315
350
|
if (command === 'recover') {
|
|
316
351
|
options.recoverCommand = next;
|
|
317
352
|
}
|
|
353
|
+
else if (command === 'harvest') {
|
|
354
|
+
options.harvestCommand = next;
|
|
355
|
+
}
|
|
318
356
|
else {
|
|
319
357
|
options.explainCommand = next;
|
|
320
358
|
}
|
|
@@ -367,16 +405,31 @@ function parseArgs(argv) {
|
|
|
367
405
|
}
|
|
368
406
|
throw new Error('sandbox requires subcommand: status');
|
|
369
407
|
}
|
|
408
|
+
if (command === 'standing-allow' && !options.standingAllowSubcommand) {
|
|
409
|
+
if (token === 'revoke') {
|
|
410
|
+
options.standingAllowSubcommand = token;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
throw new Error('standing-allow requires subcommand: revoke');
|
|
414
|
+
}
|
|
415
|
+
if (command === 'harvest' && !options.harvestSubcommand) {
|
|
416
|
+
if (token === 'list' || token === 'apply') {
|
|
417
|
+
options.harvestSubcommand = token;
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
throw new Error('harvest requires subcommand: list or apply');
|
|
421
|
+
}
|
|
370
422
|
if (command === 'judge' && !options.judgeSubcommand) {
|
|
371
423
|
if (token === 'status' ||
|
|
372
424
|
token === 'list' ||
|
|
373
425
|
token === 'use' ||
|
|
374
426
|
token === 'test' ||
|
|
427
|
+
token === 'bench' ||
|
|
375
428
|
token === 'consent') {
|
|
376
429
|
options.judgeSubcommand = token;
|
|
377
430
|
continue;
|
|
378
431
|
}
|
|
379
|
-
throw new Error('judge requires subcommand: status, list, use, test, or consent');
|
|
432
|
+
throw new Error('judge requires subcommand: status, list, use, test, bench, or consent');
|
|
380
433
|
}
|
|
381
434
|
if (command === 'config' && !options.configSubcommand) {
|
|
382
435
|
if (token === 'list' ||
|
|
@@ -458,6 +511,7 @@ Usage:
|
|
|
458
511
|
${c} dogfood [--target <dir>] [--adapter cursor|claude|codex] [--enforce] [--force]
|
|
459
512
|
${c} doctor [--target <dir>] [--adapter cursor|claude|codex] [--json] [--fix] [--dry-run]
|
|
460
513
|
${c} metrics [--target <dir>] [--json]
|
|
514
|
+
${c} quality [--target <dir>] [--corpus <path>] [--json]
|
|
461
515
|
${c} report [--target <dir>] [--since <iso>] [--until <iso>] [--limit <n>] [--json]
|
|
462
516
|
${c} recover [--target <dir>] [--since <iso>] [--fingerprint <fp>] [--command "<text>"] [--limit <n>] [--json]
|
|
463
517
|
(--limit picks the Nth recover candidate after priority ranking: local_mutation first, then recency; 1 = highest priority, default 1)
|
|
@@ -467,11 +521,15 @@ Usage:
|
|
|
467
521
|
${c} explain [--target <dir>] [--cwd <dir>] [--kind shell|tool|subagent] [--tool <name>] [--payload-json <json>] [--command <text>] [--json] [-- <command>]
|
|
468
522
|
${c} egress <start|stop|status|env> [--target <dir>] [--json]
|
|
469
523
|
${c} sandbox status [--target <dir>] [--json]
|
|
470
|
-
${c} judge <status|list|use|test|consent> [--target <dir>] [--json]
|
|
524
|
+
${c} judge <status|list|use|test|bench|consent> [--target <dir>] [--json]
|
|
525
|
+
${c} judge test [--target <dir>] [--json] [--live-probe]
|
|
471
526
|
${c} judge use <ollama|codex|claude|cursor> [--model <id>] [--endpoint <url>] [--timeout <ms>] [--accept-cloud] [--cloud-consent-approval-id <id>] [--credential project|apiKey] [--key-stdin] [--key-env <NAME>]
|
|
472
527
|
${c} judge consent <ollama|codex|claude|cursor> [--endpoint <url>]
|
|
473
|
-
${c} approve <approval-id> [--scope once|domain|path] [--path <path>] [--token <signed-token>] [--target <dir>]
|
|
528
|
+
${c} approve <approval-id> [--replay] [--scope once|domain|path|workspace-root] [--path <path>] [--token <signed-token>] [--target <dir>]
|
|
474
529
|
${c} revoke <approval-id> [--target <dir>]
|
|
530
|
+
${c} standing-allow revoke --fingerprint <fp> [--kind shell|tool|subagent] [--target <dir>]
|
|
531
|
+
${c} harvest list [--target <dir>] [--since <iso>] [--until <iso>] [--json]
|
|
532
|
+
${c} harvest apply --command "<text>" --outcome provably-benign|accepted-benign|reject [--reason <r>] [--corpus <path>] [--target <dir>]
|
|
475
533
|
`);
|
|
476
534
|
}
|
|
477
535
|
async function main() {
|
|
@@ -545,6 +603,9 @@ async function main() {
|
|
|
545
603
|
if (!options.judgeSubcommand) {
|
|
546
604
|
throw new Error('judge requires subcommand: status, list, use, or test');
|
|
547
605
|
}
|
|
606
|
+
if (options.judgeLiveProbe && options.judgeSubcommand !== 'test') {
|
|
607
|
+
throw new Error('--live-probe is only valid for judge test.');
|
|
608
|
+
}
|
|
548
609
|
const result = await runJudgeCommand({
|
|
549
610
|
targetDir: options.targetDir,
|
|
550
611
|
json: options.json,
|
|
@@ -558,6 +619,7 @@ async function main() {
|
|
|
558
619
|
credentialMode: options.credentialMode,
|
|
559
620
|
keyStdin: options.keyStdin,
|
|
560
621
|
keyEnv: options.keyEnv,
|
|
622
|
+
liveProbe: options.judgeLiveProbe,
|
|
561
623
|
});
|
|
562
624
|
if (options.json && typeof result === 'object') {
|
|
563
625
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
@@ -679,6 +741,21 @@ async function main() {
|
|
|
679
741
|
}
|
|
680
742
|
return;
|
|
681
743
|
}
|
|
744
|
+
if (command === 'quality') {
|
|
745
|
+
const report = await qualityCheck({
|
|
746
|
+
targetDir: options.targetDir,
|
|
747
|
+
corpusDir: options.corpusPath,
|
|
748
|
+
json: options.json,
|
|
749
|
+
});
|
|
750
|
+
if (options.json) {
|
|
751
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
process.stdout.write(`${formatQualityReport(report)}\n`);
|
|
755
|
+
}
|
|
756
|
+
process.exitCode = report.ok ? 0 : 1;
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
682
759
|
if (command === 'report') {
|
|
683
760
|
const report = await reportProject({
|
|
684
761
|
targetDir: options.targetDir,
|
|
@@ -695,6 +772,42 @@ async function main() {
|
|
|
695
772
|
}
|
|
696
773
|
return;
|
|
697
774
|
}
|
|
775
|
+
if (command === 'harvest') {
|
|
776
|
+
if (options.harvestSubcommand === 'list') {
|
|
777
|
+
const report = await harvestListProject({
|
|
778
|
+
targetDir: options.targetDir,
|
|
779
|
+
since: options.since,
|
|
780
|
+
until: options.until,
|
|
781
|
+
json: options.json,
|
|
782
|
+
});
|
|
783
|
+
if (options.json) {
|
|
784
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
process.stdout.write(`${formatHarvestReport(report)}\n`);
|
|
788
|
+
}
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
if (options.harvestSubcommand === 'apply') {
|
|
792
|
+
if (!options.harvestCommand) {
|
|
793
|
+
throw new Error('harvest apply requires --command.');
|
|
794
|
+
}
|
|
795
|
+
if (!options.harvestOutcome) {
|
|
796
|
+
throw new Error('harvest apply requires --outcome.');
|
|
797
|
+
}
|
|
798
|
+
const result = await harvestApplyProject({
|
|
799
|
+
targetDir: options.targetDir,
|
|
800
|
+
command: options.harvestCommand,
|
|
801
|
+
outcome: options.harvestOutcome,
|
|
802
|
+
reason: options.reason,
|
|
803
|
+
corpusPath: options.corpusPath,
|
|
804
|
+
});
|
|
805
|
+
process.stdout.write(`${result.message}\n`);
|
|
806
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
throw new Error('harvest requires subcommand: list or apply');
|
|
810
|
+
}
|
|
698
811
|
if (command === 'recover') {
|
|
699
812
|
const report = await recoverProject({
|
|
700
813
|
targetDir: options.targetDir,
|
|
@@ -804,6 +917,7 @@ async function main() {
|
|
|
804
917
|
token: options.approvalToken,
|
|
805
918
|
scope: options.approveScope,
|
|
806
919
|
scopePath: options.approvePath,
|
|
920
|
+
replay: options.approveReplay,
|
|
807
921
|
});
|
|
808
922
|
process.stdout.write(`${result.message}\n`);
|
|
809
923
|
process.exitCode = result.ok ? 0 : 1;
|
|
@@ -821,6 +935,24 @@ async function main() {
|
|
|
821
935
|
process.exitCode = result.ok ? 0 : 1;
|
|
822
936
|
return;
|
|
823
937
|
}
|
|
938
|
+
if (command === 'standing-allow') {
|
|
939
|
+
if (options.standingAllowSubcommand === 'revoke') {
|
|
940
|
+
if (!options.fingerprint) {
|
|
941
|
+
throw new Error('standing-allow revoke requires --fingerprint.');
|
|
942
|
+
}
|
|
943
|
+
const result = await revokeStandingAllow({
|
|
944
|
+
targetDir: options.targetDir,
|
|
945
|
+
fingerprint: options.fingerprint,
|
|
946
|
+
kind: options.kind === 'tool' || options.kind === 'subagent' || options.kind === 'shell'
|
|
947
|
+
? options.kind
|
|
948
|
+
: undefined,
|
|
949
|
+
});
|
|
950
|
+
process.stdout.write(`${result.message}\n`);
|
|
951
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
throw new Error('standing-allow requires subcommand: revoke');
|
|
955
|
+
}
|
|
824
956
|
throw new Error(`Unknown command: ${command}`);
|
|
825
957
|
}
|
|
826
958
|
catch (error) {
|
package/dist/commands/approve.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { loadApprovalState, loadConfigFile } from '../config-io.js';
|
|
3
|
+
import { canAutoReplay, getExecutionLeaseMs, replayShellCommand } from '../core/approval-replay.js';
|
|
4
|
+
import { consumeApprovedAfterCliReplay, createGateApprovalStore, recordApproval, } from '../core/approval-service.js';
|
|
4
5
|
import { JUDGE_CLOUD_CONSENT_REASON } from '../core/capability/reasons.js';
|
|
5
6
|
import { recordCapabilityApproval } from '../core/capability-approval.js';
|
|
6
7
|
import { recordEgressApproval } from '../core/egress-approval.js';
|
|
@@ -32,11 +33,11 @@ export async function approvePending(options) {
|
|
|
32
33
|
});
|
|
33
34
|
return { ok: result.ok, message: result.message };
|
|
34
35
|
}
|
|
35
|
-
if (options.scope === 'path') {
|
|
36
|
+
if (options.scope === 'path' || options.scope === 'workspace-root') {
|
|
36
37
|
const result = await recordCapabilityApproval({
|
|
37
38
|
approvalId: options.approvalId,
|
|
38
39
|
config,
|
|
39
|
-
scope:
|
|
40
|
+
scope: options.scope,
|
|
40
41
|
scopePath: options.scopePath,
|
|
41
42
|
token: options.token,
|
|
42
43
|
requireSignedToken: config.approvalSigning.required,
|
|
@@ -44,33 +45,51 @@ export async function approvePending(options) {
|
|
|
44
45
|
});
|
|
45
46
|
return { ok: result.ok, message: result.message };
|
|
46
47
|
}
|
|
48
|
+
const approvalStore = createGateApprovalStore(repoRoot, config);
|
|
47
49
|
const result = await recordApproval({
|
|
48
50
|
approvalId: options.approvalId,
|
|
49
51
|
config,
|
|
50
52
|
token: options.token,
|
|
51
53
|
requireSignedToken: config.approvalSigning.required,
|
|
52
|
-
store:
|
|
53
|
-
async loadPending() {
|
|
54
|
-
const filePath = pendingApprovalsPath(repoRoot, config);
|
|
55
|
-
return {
|
|
56
|
-
filePath,
|
|
57
|
-
state: await loadApprovalState(repoRoot, 'pending-approvals.json', config),
|
|
58
|
-
};
|
|
59
|
-
},
|
|
60
|
-
async loadApproved() {
|
|
61
|
-
const filePath = approvedApprovalsPath(repoRoot, config);
|
|
62
|
-
return {
|
|
63
|
-
filePath,
|
|
64
|
-
state: await loadApprovalState(repoRoot, 'approved-approvals.json', config),
|
|
65
|
-
};
|
|
66
|
-
},
|
|
67
|
-
async writePending(_filePath, state) {
|
|
68
|
-
await saveApprovalState(repoRoot, 'pending-approvals.json', state, config);
|
|
69
|
-
},
|
|
70
|
-
async writeApproved(_filePath, state) {
|
|
71
|
-
await saveApprovalState(repoRoot, 'approved-approvals.json', state, config);
|
|
72
|
-
},
|
|
73
|
-
},
|
|
54
|
+
store: approvalStore,
|
|
74
55
|
});
|
|
56
|
+
if (!result.ok) {
|
|
57
|
+
return { ok: result.ok, message: result.message };
|
|
58
|
+
}
|
|
59
|
+
if (options.replay) {
|
|
60
|
+
const approval = result.approval;
|
|
61
|
+
if (!approval) {
|
|
62
|
+
return { ok: false, message: 'Approval recorded but replay envelope is missing.' };
|
|
63
|
+
}
|
|
64
|
+
if (!canAutoReplay(config, approval.kind)) {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
message: 'Replay is not enabled for this approval kind. Retry the original action manually or enable approval.autoReplayScopes.',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (approval.kind !== 'shell' || !approval.input) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
message: 'CLI replay is only supported for shell approvals. Retry the original tool or subagent action manually.',
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const replayResult = await replayShellCommand(approval.input, approval.cwd ?? repoRoot, getExecutionLeaseMs(config));
|
|
77
|
+
const output = [replayResult.stdout, replayResult.stderr].filter(Boolean).join('\n').trim();
|
|
78
|
+
if (replayResult.exitCode === 0) {
|
|
79
|
+
await consumeApprovedAfterCliReplay({
|
|
80
|
+
approvalId: options.approvalId,
|
|
81
|
+
store: approvalStore,
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
ok: true,
|
|
85
|
+
message: `Belay replay succeeded for ${options.approvalId}. Do not retry via hooks; the one-shot grant was consumed.${output ? `\n${output}` : ''}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const timeoutNote = replayResult.timedOut ? ' Replay timed out.' : '';
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
message: `Belay replay failed for ${options.approvalId} (exit ${replayResult.exitCode}).${timeoutNote} Approval remains active for one hook retry.${output ? `\n${output}` : ''}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
75
94
|
return { ok: result.ok, message: result.message };
|
|
76
95
|
}
|
package/dist/commands/config.js
CHANGED
|
@@ -8,11 +8,13 @@ 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 { resolveJudgeTransport } from '../core/judge-runtime-detection.js';
|
|
11
12
|
import { isJudgeProviderId, JUDGE_PROVIDER_IDS, normalizeLegacyProviderId, } from '../core/verdict/judge-catalog.js';
|
|
12
13
|
import { initProject } from '../installer.js';
|
|
13
14
|
import { isBelayFloorInstalled } from './health-snapshot.js';
|
|
14
15
|
import { judgeStatus } from './judge.js';
|
|
15
16
|
import { readKeyFromStdin } from './stdin-key.js';
|
|
17
|
+
import { confirmPrompt, intro, isInteractiveTTY, selectPrompt } from './tui.js';
|
|
16
18
|
export const BELAY_CONFIG_SUBCOMMANDS = [
|
|
17
19
|
'list',
|
|
18
20
|
'get',
|
|
@@ -106,7 +108,9 @@ function listJudgeFields(judge) {
|
|
|
106
108
|
return entries;
|
|
107
109
|
}
|
|
108
110
|
function warnCloudConsentIfNeeded(judge) {
|
|
109
|
-
if (isCloudJudgeConfig(judge) &&
|
|
111
|
+
if (isCloudJudgeConfig(judge) &&
|
|
112
|
+
resolveJudgeTransport(judge) === 'http' &&
|
|
113
|
+
!hasValidCloudConsent(judge)) {
|
|
110
114
|
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
115
|
}
|
|
112
116
|
}
|
|
@@ -285,46 +289,112 @@ export async function runBelayConfigCredential(options) {
|
|
|
285
289
|
}
|
|
286
290
|
throw new Error('credential requires action: mode, set, or clear.');
|
|
287
291
|
}
|
|
288
|
-
|
|
292
|
+
function formatSelectPrompt(options) {
|
|
293
|
+
const values = options.choices.map((choice) => choice.value).join(' | ');
|
|
294
|
+
return `${options.message} [${values}] (${options.defaultValue}): `;
|
|
295
|
+
}
|
|
296
|
+
function formatConfirmPrompt(message, defaultValue) {
|
|
297
|
+
return `${message} [y | n] (${defaultValue ? 'y' : 'n'}): `;
|
|
298
|
+
}
|
|
299
|
+
function parseSelectAnswer(raw, options) {
|
|
300
|
+
const normalized = raw.trim().toLowerCase();
|
|
301
|
+
if (!normalized) {
|
|
302
|
+
return options.defaultValue;
|
|
303
|
+
}
|
|
304
|
+
const found = options.choices.find((choice) => {
|
|
305
|
+
if (choice.value.toLowerCase() === normalized) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
return choice.label?.trim().toLowerCase() === normalized;
|
|
309
|
+
});
|
|
310
|
+
if (!found) {
|
|
311
|
+
throw new Error(`Invalid choice for ${options.message}: ${raw}`);
|
|
312
|
+
}
|
|
313
|
+
return found.value;
|
|
314
|
+
}
|
|
315
|
+
async function askTextLazy(message) {
|
|
316
|
+
const rl = readline.createInterface({ input, output });
|
|
317
|
+
try {
|
|
318
|
+
return (await rl.question(message)).trimEnd();
|
|
319
|
+
}
|
|
320
|
+
finally {
|
|
321
|
+
rl.close();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function createTestPrompter(prompts) {
|
|
325
|
+
let index = 0;
|
|
326
|
+
const nextPrompt = (message) => {
|
|
327
|
+
if (index >= prompts.length) {
|
|
328
|
+
throw new Error(`unexpected config prompt: ${message}`);
|
|
329
|
+
}
|
|
330
|
+
return prompts[index++];
|
|
331
|
+
};
|
|
332
|
+
return {
|
|
333
|
+
askText: async (message) => nextPrompt(message),
|
|
334
|
+
askSelect: async (options, parseRaw) => {
|
|
335
|
+
const raw = nextPrompt(formatSelectPrompt(options));
|
|
336
|
+
return parseRaw ? parseRaw(raw, options) : parseSelectAnswer(raw, options);
|
|
337
|
+
},
|
|
338
|
+
askConfirm: async (message, defaultValue) => parseYesNo(nextPrompt(formatConfirmPrompt(message, defaultValue)), defaultValue),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
function createReadlinePrompter() {
|
|
342
|
+
return {
|
|
343
|
+
askText: askTextLazy,
|
|
344
|
+
askSelect: async (options, parseRaw) => {
|
|
345
|
+
const raw = await askTextLazy(formatSelectPrompt(options));
|
|
346
|
+
return parseRaw ? parseRaw(raw, options) : parseSelectAnswer(raw, options);
|
|
347
|
+
},
|
|
348
|
+
askConfirm: async (message, defaultValue) => parseYesNo(await askTextLazy(formatConfirmPrompt(message, defaultValue)), defaultValue),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function createTuiPrompter() {
|
|
352
|
+
return {
|
|
353
|
+
askText: askTextLazy,
|
|
354
|
+
askSelect: selectPrompt,
|
|
355
|
+
askConfirm: confirmPrompt,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
async function withConfigWizardPrompter(options, fn) {
|
|
359
|
+
if (options.prompts) {
|
|
360
|
+
return fn(createTestPrompter(options.prompts));
|
|
361
|
+
}
|
|
362
|
+
if (isInteractiveTTY()) {
|
|
363
|
+
return fn(createTuiPrompter());
|
|
364
|
+
}
|
|
365
|
+
return fn(createReadlinePrompter());
|
|
366
|
+
}
|
|
367
|
+
function writeConfigWizardBanner(options, title) {
|
|
368
|
+
if (options.skipBanner) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (isInteractiveTTY()) {
|
|
372
|
+
intro(title);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
output.write(`${title}\n`);
|
|
376
|
+
}
|
|
377
|
+
async function collectCloudJudgeWizardAnswers(prompter, judgeProviderId) {
|
|
289
378
|
if (judgeProviderId === 'ollama') {
|
|
290
379
|
return { acceptCloud: false };
|
|
291
380
|
}
|
|
292
|
-
const judgeCredentialMode =
|
|
381
|
+
const judgeCredentialMode = (await prompter.askConfirm('Use project env for credentials?', true))
|
|
293
382
|
? 'project'
|
|
294
383
|
: 'apiKey';
|
|
295
|
-
const optionalEndpoint = (await
|
|
384
|
+
const optionalEndpoint = (await prompter.askText('Judge endpoint URL (optional): ')).trim();
|
|
296
385
|
const judgeEndpoint = optionalEndpoint || undefined;
|
|
297
386
|
if (judgeCredentialMode === 'apiKey') {
|
|
298
|
-
const key = await
|
|
387
|
+
const key = await prompter.askText('Paste API key (hidden input not available in all shells): ');
|
|
299
388
|
if (key.trim()) {
|
|
300
389
|
process.env.BELAY_CONFIG_WIZARD_JUDGE_KEY = key.trim();
|
|
301
390
|
}
|
|
302
391
|
}
|
|
303
392
|
let acceptCloud = false;
|
|
304
393
|
if (judgeEndpoint) {
|
|
305
|
-
acceptCloud =
|
|
394
|
+
acceptCloud = await prompter.askConfirm('Accept cloud judge egress (redacted commands leave the repo)?', false);
|
|
306
395
|
}
|
|
307
396
|
return { judgeCredentialMode, judgeEndpoint, acceptCloud };
|
|
308
397
|
}
|
|
309
|
-
async function withConfigPrompter(fn, prompts) {
|
|
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
|
-
}
|
|
320
|
-
const rl = readline.createInterface({ input, output });
|
|
321
|
-
try {
|
|
322
|
-
return await fn((message) => rl.question(message));
|
|
323
|
-
}
|
|
324
|
-
finally {
|
|
325
|
-
rl.close();
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
398
|
export async function resolveBelayConfigInteractiveMode(repoRoot) {
|
|
329
399
|
try {
|
|
330
400
|
return (await isBelayFloorInstalled({ targetDir: repoRoot })) ? 'judge-only' : 'full';
|
|
@@ -333,16 +403,26 @@ export async function resolveBelayConfigInteractiveMode(repoRoot) {
|
|
|
333
403
|
return 'full';
|
|
334
404
|
}
|
|
335
405
|
}
|
|
336
|
-
async function
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
406
|
+
async function runBelayConfigFullWithWizard(prompter, options) {
|
|
407
|
+
writeConfigWizardBanner(options, 'belay config');
|
|
408
|
+
const adapter = await prompter.askSelect({
|
|
409
|
+
message: 'Adapter',
|
|
410
|
+
defaultValue: 'cursor',
|
|
411
|
+
choices: [{ value: 'cursor' }, { value: 'claude' }, { value: 'codex' }],
|
|
412
|
+
});
|
|
413
|
+
const scope = await prompter.askSelect({
|
|
414
|
+
message: 'Install scope',
|
|
415
|
+
defaultValue: 'project',
|
|
416
|
+
choices: [{ value: 'project' }, { value: 'global' }],
|
|
417
|
+
});
|
|
418
|
+
const withSkill = await prompter.askConfirm('Install SKILL.md and slash commands?', true);
|
|
343
419
|
const defaultJudgeProviderId = defaultJudgeProviderForAdapter(adapter);
|
|
344
|
-
const judgeProviderId =
|
|
345
|
-
|
|
420
|
+
const judgeProviderId = await prompter.askSelect({
|
|
421
|
+
message: 'Judge provider',
|
|
422
|
+
defaultValue: defaultJudgeProviderId,
|
|
423
|
+
choices: JUDGE_PROVIDER_IDS.map((providerId) => ({ value: providerId })),
|
|
424
|
+
}, (raw, selectOptions) => parseJudgeProviderId(raw, selectOptions.defaultValue));
|
|
425
|
+
const { judgeCredentialMode, judgeEndpoint, acceptCloud } = await collectCloudJudgeWizardAnswers(prompter, judgeProviderId);
|
|
346
426
|
const initOptions = buildInitOptionsFromConfigAnswers({
|
|
347
427
|
adapter,
|
|
348
428
|
scope,
|
|
@@ -362,19 +442,22 @@ async function runBelayConfigFullWithPrompter(ask, options) {
|
|
|
362
442
|
}
|
|
363
443
|
return result;
|
|
364
444
|
}
|
|
365
|
-
async function
|
|
366
|
-
|
|
367
|
-
output.write('belay config (judge only)\n');
|
|
368
|
-
}
|
|
445
|
+
async function runBelayConfigJudgeOnlyWithWizard(prompter, options, repoRoot, config, adapter) {
|
|
446
|
+
writeConfigWizardBanner(options, 'belay config (judge only)');
|
|
369
447
|
const defaultJudgeProviderId = defaultJudgeProviderForAdapter(adapter);
|
|
370
|
-
const judgeProviderId =
|
|
371
|
-
|
|
448
|
+
const judgeProviderId = await prompter.askSelect({
|
|
449
|
+
message: 'Judge provider',
|
|
450
|
+
defaultValue: defaultJudgeProviderId,
|
|
451
|
+
choices: JUDGE_PROVIDER_IDS.map((providerId) => ({ value: providerId })),
|
|
452
|
+
}, (raw, selectOptions) => parseJudgeProviderId(raw, selectOptions.defaultValue));
|
|
453
|
+
const { judgeCredentialMode, judgeEndpoint, acceptCloud } = await collectCloudJudgeWizardAnswers(prompter, judgeProviderId);
|
|
372
454
|
const patch = resolveJudgeUsePatch(config.judge, {
|
|
373
455
|
providerId: judgeProviderId,
|
|
374
456
|
endpoint: judgeEndpoint,
|
|
375
457
|
credentialMode: judgeCredentialMode,
|
|
376
458
|
acceptCloud: acceptCloud && Boolean(judgeEndpoint),
|
|
377
|
-
|
|
459
|
+
// `prompts` is a scripted stand-in for interactive responses in tests.
|
|
460
|
+
interactiveTTY: isInteractiveTTY() || Boolean(options.prompts),
|
|
378
461
|
interactiveConsentApproved: acceptCloud && Boolean(judgeEndpoint),
|
|
379
462
|
});
|
|
380
463
|
if (patch.errors.length > 0) {
|
|
@@ -399,13 +482,13 @@ async function runBelayConfigJudgeOnlyWithPrompter(ask, options, repoRoot, confi
|
|
|
399
482
|
return { repoRoot, adapter };
|
|
400
483
|
}
|
|
401
484
|
export async function runBelayConfigFullInteractive(options = {}) {
|
|
402
|
-
return
|
|
485
|
+
return withConfigWizardPrompter(options, (prompter) => runBelayConfigFullWithWizard(prompter, options));
|
|
403
486
|
}
|
|
404
487
|
export async function runBelayConfigJudgeOnlyInteractive(options = {}) {
|
|
405
488
|
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
406
489
|
const config = await loadConfigFile(repoRoot);
|
|
407
490
|
const adapter = resolveAdapterName(config);
|
|
408
|
-
return
|
|
491
|
+
return withConfigWizardPrompter(options, (prompter) => runBelayConfigJudgeOnlyWithWizard(prompter, options, repoRoot, config, adapter));
|
|
409
492
|
}
|
|
410
493
|
export async function runBelayConfigInteractive(options = {}) {
|
|
411
494
|
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
@@ -427,16 +510,16 @@ export async function runBelayConfigInteractive(options = {}) {
|
|
|
427
510
|
skipBanner: true,
|
|
428
511
|
});
|
|
429
512
|
}
|
|
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);
|
|
513
|
+
return withConfigWizardPrompter(options, async (prompter) => {
|
|
514
|
+
writeConfigWizardBanner(options, 'belay config');
|
|
515
|
+
const judgeOnly = await prompter.askConfirm('Configure judge only?', true);
|
|
516
|
+
if (!judgeOnly) {
|
|
517
|
+
return runBelayConfigFullWithWizard(prompter, { ...options, skipBanner: true });
|
|
437
518
|
}
|
|
438
|
-
|
|
439
|
-
|
|
519
|
+
const config = await loadConfigFile(repoRoot);
|
|
520
|
+
const adapter = resolveAdapterName(config);
|
|
521
|
+
return runBelayConfigJudgeOnlyWithWizard(prompter, { ...options, skipBanner: true }, repoRoot, config, adapter);
|
|
522
|
+
});
|
|
440
523
|
}
|
|
441
524
|
return runBelayConfigFullInteractive(options);
|
|
442
525
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -10,7 +10,7 @@ import { approvedApprovalsPath, belayStateDir, detectAdapterName, loadLayeredCon
|
|
|
10
10
|
import { detectFenceDrift, summarizeAuditVisibility } from '../core/audit-summary.js';
|
|
11
11
|
import { defaultControlPlaneDir } from '../core/config.js';
|
|
12
12
|
import { verifyIntegrityManifest } from '../core/integrity.js';
|
|
13
|
-
import { diagnoseJudge } from '../core/judge-doctor.js';
|
|
13
|
+
import { diagnoseJudge, stopJudgeSessionBrokers } from '../core/judge-doctor.js';
|
|
14
14
|
import { getManagedHookEntries } from '../defaults.js';
|
|
15
15
|
import { resolveNodeBinary } from '../node-resolution.js';
|
|
16
16
|
import { egressStatus } from '../services/egress-service.js';
|
|
@@ -239,6 +239,16 @@ export async function doctorProject(options = {}) {
|
|
|
239
239
|
else {
|
|
240
240
|
notes.push('No orphan approval cleanup actions were needed.');
|
|
241
241
|
}
|
|
242
|
+
const judgeStateDir = belayStateDir(loadedConfig, repoLocalDir);
|
|
243
|
+
if (options.dryRun !== true) {
|
|
244
|
+
const stoppedBrokers = await stopJudgeSessionBrokers(repoRoot, judgeStateDir);
|
|
245
|
+
if (stoppedBrokers > 0) {
|
|
246
|
+
notes.push(`Stopped judge session broker artifacts for ${repoRoot}.`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
notes.push('Dry run: would stop judge session broker and clear kill switch if present.');
|
|
251
|
+
}
|
|
242
252
|
}
|
|
243
253
|
let dogfood = null;
|
|
244
254
|
if (loadedConfig) {
|