@guilz-dev/belay 0.9.3 → 0.10.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 +38 -3
- package/dist/adapters/codex/hooks.d.ts +1 -0
- package/dist/adapters/codex/hooks.js +3 -0
- package/dist/adapters/cursor/dispatcher-generation.d.ts +3 -0
- package/dist/adapters/cursor/dispatcher-generation.js +5 -0
- package/dist/adapters/cursor/hook-dispatch-entry.d.ts +10 -0
- package/dist/adapters/cursor/hook-dispatch-entry.js +137 -0
- package/dist/adapters/cursor/hook-router.d.ts +23 -0
- package/dist/adapters/cursor/hook-router.js +273 -0
- package/dist/adapters/cursor/hooks.d.ts +5 -1
- package/dist/adapters/cursor/hooks.js +112 -10
- package/dist/adapters/cursor/routing-config-trust.d.ts +1 -0
- package/dist/adapters/cursor/routing-config-trust.js +67 -0
- package/dist/adapters/cursor/routing-layout.d.ts +5 -0
- package/dist/adapters/cursor/routing-layout.js +29 -0
- package/dist/adapters/cursor/runtime-entry.d.ts +17 -0
- package/dist/adapters/cursor/runtime-entry.js +74 -49
- package/dist/adapters/layouts/scope.d.ts +5 -0
- package/dist/adapters/layouts/scope.js +71 -0
- package/dist/adapters/shared/gate-runtime.js +29 -17
- package/dist/bundle/claude-runtime.mjs +2428 -2072
- package/dist/bundle/codex-runtime.mjs +2440 -2084
- package/dist/bundle/cursor-dispatcher.mjs +517 -0
- package/dist/bundle/cursor-runtime.mjs +2521 -2133
- package/dist/cli.js +53 -4
- package/dist/commands/approval-token.d.ts +10 -0
- package/dist/commands/approval-token.js +26 -0
- package/dist/commands/audit.d.ts +2 -1
- package/dist/commands/audit.js +2 -2
- package/dist/commands/config.d.ts +1 -1
- package/dist/commands/config.js +28 -2
- package/dist/commands/doctor.js +149 -17
- package/dist/commands/dogfood-check.d.ts +3 -0
- package/dist/commands/dogfood-check.js +116 -0
- package/dist/commands/dogfood.d.ts +1 -0
- package/dist/commands/dogfood.js +4 -3
- package/dist/commands/health-snapshot.js +9 -4
- package/dist/commands/judge.js +2 -2
- package/dist/commands/recovery-checkpoints.d.ts +3 -3
- package/dist/commands/recovery-checkpoints.js +2 -11
- package/dist/config-io.d.ts +1 -0
- package/dist/config-io.js +19 -3
- package/dist/core/audit-legacy-archive.js +43 -25
- package/dist/core/classify-tool.js +16 -8
- package/dist/core/dogfood-environment.d.ts +8 -0
- package/dist/core/dogfood-environment.js +55 -0
- package/dist/core/effect-ir/argv-delegate.d.ts +9 -0
- package/dist/core/effect-ir/argv-delegate.js +42 -0
- package/dist/core/effect-ir/shell-lower/argv-delegate-gate.d.ts +3 -0
- package/dist/core/effect-ir/shell-lower/argv-delegate-gate.js +40 -0
- package/dist/core/effect-ir/shell-lower/augment.d.ts +4 -0
- package/dist/core/effect-ir/shell-lower/augment.js +97 -0
- package/dist/core/effect-ir/shell-lower/context.d.ts +10 -0
- package/dist/core/effect-ir/shell-lower/context.js +1 -0
- package/dist/core/effect-ir/shell-lower/decode-process.d.ts +9 -0
- package/dist/core/effect-ir/shell-lower/decode-process.js +201 -0
- package/dist/core/effect-ir/shell-lower/decoders/belay.d.ts +2 -0
- package/dist/core/effect-ir/shell-lower/decoders/belay.js +41 -0
- package/dist/core/effect-ir/shell-lower/decoders/builtins.d.ts +7 -0
- package/dist/core/effect-ir/shell-lower/decoders/builtins.js +194 -0
- package/dist/core/effect-ir/shell-lower/decoders/decode-package-exec.d.ts +4 -0
- package/dist/core/effect-ir/shell-lower/decoders/decode-package-exec.js +68 -0
- package/dist/core/effect-ir/shell-lower/decoders/docker.d.ts +3 -0
- package/dist/core/effect-ir/shell-lower/decoders/docker.js +43 -0
- package/dist/core/effect-ir/shell-lower/decoders/filesystem.d.ts +9 -0
- package/dist/core/effect-ir/shell-lower/decoders/filesystem.js +217 -0
- package/dist/core/effect-ir/shell-lower/decoders/prisma.d.ts +2 -0
- package/dist/core/effect-ir/shell-lower/decoders/prisma.js +54 -0
- package/dist/core/effect-ir/shell-lower/decoders/ruby.d.ts +11 -0
- package/dist/core/effect-ir/shell-lower/decoders/ruby.js +147 -0
- package/dist/core/effect-ir/shell-lower/decoders/toolchain.d.ts +7 -0
- package/dist/core/effect-ir/shell-lower/decoders/toolchain.js +155 -0
- package/dist/core/effect-ir/shell-lower/requirement.d.ts +12 -0
- package/dist/core/effect-ir/shell-lower/requirement.js +65 -0
- package/dist/core/effect-ir/shell-lower/segment.d.ts +11 -0
- package/dist/core/effect-ir/shell-lower/segment.js +82 -0
- package/dist/core/effect-ir/shell-lower/tokens.d.ts +26 -0
- package/dist/core/effect-ir/shell-lower/tokens.js +183 -0
- package/dist/core/effect-ir/shell-lower.d.ts +2 -7
- package/dist/core/effect-ir/shell-lower.js +60 -1502
- package/dist/core/egress-approval.js +0 -18
- package/dist/core/glob.js +32 -16
- package/dist/core/integrity.d.ts +2 -2
- package/dist/core/integrity.js +52 -12
- package/dist/core/notify.d.ts +8 -2
- package/dist/core/notify.js +63 -7
- package/dist/core/recovery/operator-guidance.js +4 -4
- package/dist/core/replay-scrub.d.ts +2 -0
- package/dist/core/replay-scrub.js +8 -0
- package/dist/core/repo-config-trust.d.ts +31 -0
- package/dist/core/repo-config-trust.js +152 -0
- package/dist/core/shell-substitution.js +2 -2
- package/dist/core/shell-unparseable.js +3 -1
- package/dist/corpus/benign-probe-cores.d.ts +1 -1
- package/dist/corpus/benign-probe-cores.js +0 -1
- package/dist/defaults.d.ts +7 -3
- package/dist/defaults.js +29 -47
- package/dist/installer/bootstrap.d.ts +1 -0
- package/dist/installer/bootstrap.js +2 -2
- package/dist/installer/runtime-artifacts.js +43 -13
- package/dist/installer/scope-config.js +2 -2
- package/dist/installer.js +48 -13
- package/dist/node-resolution.d.ts +1 -0
- package/dist/node-resolution.js +61 -0
- package/dist/templates.d.ts +7 -4
- package/dist/templates.js +42 -4
- package/dist/types.d.ts +20 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -1
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import process from 'node:process';
|
|
3
3
|
import { CLI_COMMAND } from './branding.js';
|
|
4
|
+
import { issuePendingApprovalToken } from './commands/approval-token.js';
|
|
4
5
|
import { approvePending } from './commands/approve.js';
|
|
5
6
|
import { auditProject, formatAuditReport } from './commands/audit.js';
|
|
6
7
|
import { doctorProject, formatDoctorReport } from './commands/doctor.js';
|
|
7
|
-
import { dogfoodProject, formatDogfoodResult } from './commands/dogfood.js';
|
|
8
|
+
import { checkDogfoodProject, dogfoodProject, formatDogfoodCheckResult, formatDogfoodResult, } from './commands/dogfood.js';
|
|
8
9
|
import { explainCommand, formatExplainReport } from './commands/explain.js';
|
|
9
10
|
import { formatHarvestReport, harvestApplyProject, harvestListProject } from './commands/harvest.js';
|
|
10
11
|
import { formatMetricsReport, metricsProject } from './commands/metrics.js';
|
|
@@ -51,6 +52,13 @@ function parseArgs(argv) {
|
|
|
51
52
|
options.force = true;
|
|
52
53
|
continue;
|
|
53
54
|
}
|
|
55
|
+
if (token === '--check') {
|
|
56
|
+
if (command !== 'dogfood') {
|
|
57
|
+
throw new Error('--check is only valid for dogfood.');
|
|
58
|
+
}
|
|
59
|
+
options.dogfoodCheck = true;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
54
62
|
if (token === '--adapter') {
|
|
55
63
|
const next = rest[index + 1];
|
|
56
64
|
if (!next || !['cursor', 'claude', 'codex'].includes(next)) {
|
|
@@ -472,12 +480,13 @@ function parseArgs(argv) {
|
|
|
472
480
|
token === 'get' ||
|
|
473
481
|
token === 'set' ||
|
|
474
482
|
token === 'unset' ||
|
|
483
|
+
token === 'trust' ||
|
|
475
484
|
token === 'credential' ||
|
|
476
485
|
token === 'judge') {
|
|
477
486
|
options.configSubcommand = token;
|
|
478
487
|
continue;
|
|
479
488
|
}
|
|
480
|
-
throw new Error('config requires subcommand: list, get, set, unset, credential, or judge');
|
|
489
|
+
throw new Error('config requires subcommand: list, get, set, unset, trust, credential, or judge');
|
|
481
490
|
}
|
|
482
491
|
if (command === 'config' &&
|
|
483
492
|
options.configSubcommand === 'credential' &&
|
|
@@ -519,7 +528,8 @@ function parseArgs(argv) {
|
|
|
519
528
|
options.judgeUseProvider = token;
|
|
520
529
|
continue;
|
|
521
530
|
}
|
|
522
|
-
if ((command === 'revoke' || command === 'approve'
|
|
531
|
+
if ((command === 'revoke' || command === 'approve' || command === 'approval-token') &&
|
|
532
|
+
!options.approvalId) {
|
|
523
533
|
options.approvalId = token;
|
|
524
534
|
continue;
|
|
525
535
|
}
|
|
@@ -534,10 +544,11 @@ function printHelp() {
|
|
|
534
544
|
Usage:
|
|
535
545
|
${c} init [--target <dir>] [--adapter cursor|claude|codex] [--scope project|global] [--preset strict|standard|audit-first|l1-full-recommended] [--judge-profile local-ollama|cursor|claude|codex] [--judge-provider ollama|openai-compatible] [--judge-model <id>] [--judge-endpoint <url>] [--accept-cloud-judge] [--migrate-judge-default] [--with-skill] [--dogfood]
|
|
536
546
|
${c} config [--target <dir>] [--json]
|
|
537
|
-
${c} config list|get|set|unset|judge [--target <dir>] [--json]
|
|
547
|
+
${c} config list|get|set|unset|trust|judge [--target <dir>] [--json]
|
|
538
548
|
${c} config get <judge.path> [--target <dir>] [--json]
|
|
539
549
|
${c} config set <judge.path> <value> [--target <dir>]
|
|
540
550
|
${c} config unset <judge.path> [--target <dir>]
|
|
551
|
+
${c} config trust [--target <dir>]
|
|
541
552
|
${c} config credential mode <project|apiKey> [--target <dir>]
|
|
542
553
|
${c} config credential set [--key-stdin] [--key-env <NAME>] [--target <dir>]
|
|
543
554
|
${c} config credential clear [--target <dir>]
|
|
@@ -547,6 +558,7 @@ Usage:
|
|
|
547
558
|
${c} uninstall [--target <dir>] [--adapter cursor] [--scope project|global]
|
|
548
559
|
${c} where [--target <dir>] [--adapter cursor|claude|codex] [--scope project|global] [--json]
|
|
549
560
|
${c} dogfood [--target <dir>] [--adapter cursor|claude|codex] [--enforce] [--force]
|
|
561
|
+
${c} dogfood --check --since <iso> [--target <dir>] [--adapter cursor|claude|codex] [--json]
|
|
550
562
|
${c} doctor [--target <dir>] [--adapter cursor|claude|codex] [--json] [--fix] [--dry-run]
|
|
551
563
|
${c} metrics [--target <dir>] [--json]
|
|
552
564
|
${c} quality [--target <dir>] [--corpus <path>] [--json]
|
|
@@ -568,6 +580,7 @@ Usage:
|
|
|
568
580
|
${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>]
|
|
569
581
|
${c} judge consent <ollama|codex|claude|cursor> [--endpoint <url>]
|
|
570
582
|
${c} approve <approval-id> [--replay] [--scope once|domain|path|workspace-root] [--path <path>] [--token <signed-token>] [--target <dir>]
|
|
583
|
+
${c} approval-token <approval-id> [--target <dir>] [--json]
|
|
571
584
|
${c} revoke <approval-id> [--target <dir>]
|
|
572
585
|
${c} standing-allow revoke --fingerprint <fp> [--kind shell|tool|subagent] [--target <dir>]
|
|
573
586
|
${c} harvest list [--target <dir>] [--since <iso>] [--until <iso>] [--json]
|
|
@@ -615,6 +628,30 @@ async function main() {
|
|
|
615
628
|
return;
|
|
616
629
|
}
|
|
617
630
|
if (command === 'dogfood') {
|
|
631
|
+
if (options.dogfoodCheck) {
|
|
632
|
+
if (options.enforce) {
|
|
633
|
+
throw new Error('--check conflicts with --enforce.');
|
|
634
|
+
}
|
|
635
|
+
if (options.force) {
|
|
636
|
+
throw new Error('--check conflicts with --force.');
|
|
637
|
+
}
|
|
638
|
+
if (!options.since) {
|
|
639
|
+
throw new Error('--check requires --since <iso>.');
|
|
640
|
+
}
|
|
641
|
+
const report = await checkDogfoodProject({
|
|
642
|
+
targetDir: options.targetDir,
|
|
643
|
+
adapter: options.adapter,
|
|
644
|
+
since: options.since,
|
|
645
|
+
});
|
|
646
|
+
if (options.json) {
|
|
647
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
process.stdout.write(formatDogfoodCheckResult(report));
|
|
651
|
+
}
|
|
652
|
+
process.exitCode = report.ok ? 0 : 1;
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
618
655
|
const result = await dogfoodProject({
|
|
619
656
|
targetDir: options.targetDir,
|
|
620
657
|
enforce: options.enforce,
|
|
@@ -1031,6 +1068,18 @@ async function main() {
|
|
|
1031
1068
|
process.exitCode = result.ok ? 0 : 1;
|
|
1032
1069
|
return;
|
|
1033
1070
|
}
|
|
1071
|
+
if (command === 'approval-token') {
|
|
1072
|
+
if (!options.approvalId) {
|
|
1073
|
+
throw new Error('approval-token requires an approval ID.');
|
|
1074
|
+
}
|
|
1075
|
+
const result = await issuePendingApprovalToken({
|
|
1076
|
+
targetDir: options.targetDir,
|
|
1077
|
+
approvalId: options.approvalId,
|
|
1078
|
+
});
|
|
1079
|
+
process.stdout.write(options.json ? `${JSON.stringify(result, null, 2)}\n` : `${result.message}\n`);
|
|
1080
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1034
1083
|
if (command === 'revoke') {
|
|
1035
1084
|
if (!options.approvalId) {
|
|
1036
1085
|
throw new Error('revoke requires an approval ID.');
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface IssuePendingApprovalTokenOptions {
|
|
2
|
+
targetDir?: string;
|
|
3
|
+
approvalId: string;
|
|
4
|
+
}
|
|
5
|
+
export interface IssuePendingApprovalTokenResult {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
message: string;
|
|
8
|
+
token?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function issuePendingApprovalToken(options: IssuePendingApprovalTokenOptions): Promise<IssuePendingApprovalTokenResult>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { loadApprovalState, loadConfigFile } from '../config-io.js';
|
|
3
|
+
import { isExpired } from '../core/approval.js';
|
|
4
|
+
import { issueApprovalToken } from '../core/approval-token.js';
|
|
5
|
+
import { configuredControlPlaneDir } from '../core/config.js';
|
|
6
|
+
import { canonicalPath } from '../core/path-utils.js';
|
|
7
|
+
export async function issuePendingApprovalToken(options) {
|
|
8
|
+
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
9
|
+
const config = await loadConfigFile(repoRoot);
|
|
10
|
+
const pending = await loadApprovalState(repoRoot, 'pending-approvals.json', config);
|
|
11
|
+
const approval = pending.approvals.find((entry) => entry.approvalId === options.approvalId);
|
|
12
|
+
if (!approval || isExpired(approval)) {
|
|
13
|
+
return { ok: false, message: `Pending approval not found: ${options.approvalId}` };
|
|
14
|
+
}
|
|
15
|
+
if (canonicalPath(approval.repoRoot) !== canonicalPath(repoRoot)) {
|
|
16
|
+
return { ok: false, message: `Approval repository does not match: ${options.approvalId}` };
|
|
17
|
+
}
|
|
18
|
+
const token = await issueApprovalToken({
|
|
19
|
+
approvalId: approval.approvalId,
|
|
20
|
+
fingerprint: approval.fingerprint,
|
|
21
|
+
repoRoot: approval.repoRoot,
|
|
22
|
+
issuedAt: approval.createdAt,
|
|
23
|
+
expiresAt: approval.expiresAt,
|
|
24
|
+
}, configuredControlPlaneDir(config));
|
|
25
|
+
return { ok: true, token, message: token };
|
|
26
|
+
}
|
package/dist/commands/audit.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AuditRecord } from '../core/audit-types.js';
|
|
2
|
+
import type { AdapterName } from '../types.js';
|
|
2
3
|
export type AuditSubcommand = 'query' | 'summarize' | 'replay';
|
|
3
4
|
export interface AuditOptions {
|
|
4
5
|
targetDir?: string;
|
|
@@ -18,7 +19,7 @@ export interface AuditOptions {
|
|
|
18
19
|
limit?: number;
|
|
19
20
|
configPath?: string;
|
|
20
21
|
}
|
|
21
|
-
export declare function loadAuditRecords(repoRoot: string): Promise<AuditRecord[]>;
|
|
22
|
+
export declare function loadAuditRecords(repoRoot: string, adapter?: AdapterName): Promise<AuditRecord[]>;
|
|
22
23
|
export declare function auditProject(options: AuditOptions): Promise<{
|
|
23
24
|
subcommand: string;
|
|
24
25
|
records: AuditRecord[];
|
package/dist/commands/audit.js
CHANGED
|
@@ -7,8 +7,8 @@ import { parseAuditNdjson, toAuditRecord } from '../core/audit-metrics.js';
|
|
|
7
7
|
import { buildApprovalRoundTrips, filterAuditRecords, summarizeRoundTrips, } from '../core/audit-query.js';
|
|
8
8
|
import { mergeConfig } from '../core/config.js';
|
|
9
9
|
import { diffReclassification } from '../core/reclassify.js';
|
|
10
|
-
export async function loadAuditRecords(repoRoot) {
|
|
11
|
-
const config = await loadConfigFile(repoRoot);
|
|
10
|
+
export async function loadAuditRecords(repoRoot, adapter) {
|
|
11
|
+
const config = await loadConfigFile(repoRoot, adapter);
|
|
12
12
|
const auditLogPath = path.join(repoRoot, config.audit.logPath);
|
|
13
13
|
let raw = '';
|
|
14
14
|
try {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type JudgeProviderId } from '../core/verdict/judge-catalog.js';
|
|
2
2
|
import type { AdapterName, InitOptions } from '../types.js';
|
|
3
3
|
import { type SelectOptions } from './tui.js';
|
|
4
|
-
export declare const BELAY_CONFIG_SUBCOMMANDS: readonly ["list", "get", "set", "unset", "credential", "judge"];
|
|
4
|
+
export declare const BELAY_CONFIG_SUBCOMMANDS: readonly ["list", "get", "set", "unset", "trust", "credential", "judge"];
|
|
5
5
|
export type BelayConfigSubcommand = (typeof BELAY_CONFIG_SUBCOMMANDS)[number];
|
|
6
6
|
export interface BelayConfigOptions {
|
|
7
7
|
targetDir?: string;
|
package/dist/commands/config.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
import { stdin as input, stdout as output } from 'node:process';
|
|
3
4
|
import readline from 'node:readline/promises';
|
|
4
|
-
import { loadConfigFile, repoLocalStateDirFor, resolveAdapterName,
|
|
5
|
+
import { configPathFor, detectAdapterName, loadConfigFile, repoLocalStateDirFor, resolveAdapterName, writeTrustedConfigFile, } from '../config-io.js';
|
|
5
6
|
import { appendCliAuditEvent } from '../core/audit-io.js';
|
|
6
7
|
import { belayStateDir, normalizeJudgeConfig } from '../core/config.js';
|
|
7
8
|
import { clearJudgeCredentialStore, writeJudgeCredentialStore } from '../core/credential-store.js';
|
|
@@ -9,6 +10,7 @@ import { refreshIntegrityIfPinned } from '../core/integrity.js';
|
|
|
9
10
|
import { defaultJudgeProviderForAdapter, hasValidCloudConsent, isCloudJudgeConfig, resolveJudgeUsePatch, } from '../core/judge-config.js';
|
|
10
11
|
import { rejectDeprecatedJudgeModelAuto } from '../core/judge-model-policy.js';
|
|
11
12
|
import { detectJudgeRuntimeCapabilities, resolveJudgeTransport, } from '../core/judge-runtime-detection.js';
|
|
13
|
+
import { trustRepoConfig } from '../core/repo-config-trust.js';
|
|
12
14
|
import { getJudgeProviderSpec, isJudgeProviderId, JUDGE_PROVIDER_IDS, normalizeLegacyProviderId, } from '../core/verdict/judge-catalog.js';
|
|
13
15
|
import { normalizeJudgeRuntimeConfig } from '../core/verdict/judge-runtime-config.js';
|
|
14
16
|
import { initProject } from '../installer.js';
|
|
@@ -21,6 +23,7 @@ export const BELAY_CONFIG_SUBCOMMANDS = [
|
|
|
21
23
|
'get',
|
|
22
24
|
'set',
|
|
23
25
|
'unset',
|
|
26
|
+
'trust',
|
|
24
27
|
'credential',
|
|
25
28
|
'judge',
|
|
26
29
|
];
|
|
@@ -125,7 +128,7 @@ function warnCloudConsentIfNeeded(judge) {
|
|
|
125
128
|
}
|
|
126
129
|
async function persistJudge(repoRoot, config, judge, adapter) {
|
|
127
130
|
const updated = { ...config, judge: normalizeJudgeConfig(judge) };
|
|
128
|
-
await
|
|
131
|
+
await writeTrustedConfigFile(repoRoot, updated, adapter);
|
|
129
132
|
await refreshIntegrityIfPinned(repoRoot, updated);
|
|
130
133
|
return updated;
|
|
131
134
|
}
|
|
@@ -651,6 +654,29 @@ export async function runBelayConfig(options = {}) {
|
|
|
651
654
|
return runBelayConfigInteractive({ targetDir: options.targetDir });
|
|
652
655
|
}
|
|
653
656
|
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
657
|
+
if (options.subcommand === 'trust') {
|
|
658
|
+
const adapter = detectAdapterName(repoRoot);
|
|
659
|
+
const configPath = configPathFor(repoRoot, adapter);
|
|
660
|
+
let rawConfig;
|
|
661
|
+
try {
|
|
662
|
+
rawConfig = JSON.parse(await readFile(configPath, 'utf8'));
|
|
663
|
+
}
|
|
664
|
+
catch (error) {
|
|
665
|
+
if (error?.code === 'ENOENT') {
|
|
666
|
+
throw new Error(`Missing repository config: ${configPath}`);
|
|
667
|
+
}
|
|
668
|
+
throw new Error(`Repository config is malformed JSON: ${configPath}`);
|
|
669
|
+
}
|
|
670
|
+
const record = await trustRepoConfig(repoRoot, adapter, rawConfig);
|
|
671
|
+
const merged = await loadConfigFile(repoRoot, adapter);
|
|
672
|
+
await refreshIntegrityIfPinned(repoRoot, merged);
|
|
673
|
+
return [
|
|
674
|
+
'Repository config trusted:',
|
|
675
|
+
JSON.stringify(rawConfig, null, 2),
|
|
676
|
+
`Trusted fingerprint: ${record.repoConfigFingerprint}`,
|
|
677
|
+
`Repository: ${record.repoRoot}`,
|
|
678
|
+
].join('\n');
|
|
679
|
+
}
|
|
654
680
|
const config = await loadConfigFile(repoRoot);
|
|
655
681
|
if (options.subcommand === 'list') {
|
|
656
682
|
const entries = listJudgeFields(config.judge);
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { getClaudeManagedHookEntries } from '../adapters/claude/hooks.js';
|
|
5
|
-
import { getCodexManagedHookEntries } from '../adapters/codex/hooks.js';
|
|
6
|
-
import {
|
|
5
|
+
import { codexHooksTomlIncludesCommand, getCodexManagedHookEntries, } from '../adapters/codex/hooks.js';
|
|
6
|
+
import { hasCurrentCursorDispatcherGeneration } from '../adapters/cursor/dispatcher-generation.js';
|
|
7
|
+
import { hasLegacyCursorDoubleShellGates, hasManagedCursorHookEntries, } from '../adapters/cursor/hooks.js';
|
|
7
8
|
import { getAdapterLayout } from '../adapters/layouts/index.js';
|
|
8
9
|
import { protectedArtifactRoots } from '../adapters/layouts/protected-paths.js';
|
|
9
10
|
import { resolveScopedPaths } from '../adapters/layouts/scope.js';
|
|
10
11
|
import { cleanupOrphanApprovalState } from '../cleanup-orphans.js';
|
|
11
|
-
import { approvedApprovalsPath, belayStateDir, detectAdapterName, loadLayeredConfig, pendingApprovalsPath, repoLocalStateDirFor,
|
|
12
|
+
import { approvedApprovalsPath, belayStateDir, detectAdapterName, loadLayeredConfig, pendingApprovalsPath, repoLocalStateDirFor, writeTrustedConfigFile, } from '../config-io.js';
|
|
12
13
|
import { approvalSigningKeyPath } from '../core/approval-token.js';
|
|
13
14
|
import { auditRecordHasLegacyCorrelationPlaceholders } from '../core/audit-legacy-archive.js';
|
|
14
15
|
import { detectFenceDrift, summarizeAuditVisibility } from '../core/audit-summary.js';
|
|
15
16
|
import { inspectBoundaryAttestationFile } from '../core/capability/boundary-attestation-sign.js';
|
|
16
17
|
import { boundaryAttestationPath, boundarySessionStatus, } from '../core/capability/boundary-session.js';
|
|
17
18
|
import { configuredControlPlaneDir, defaultControlPlaneDir, hasForbiddenShellOverrideLists, stripForbiddenShellOverrideLists, } from '../core/config.js';
|
|
18
|
-
import {
|
|
19
|
+
import { detectUndogfoodedLinkedWorktrees } from '../core/dogfood-environment.js';
|
|
20
|
+
import { runtimeIntegrityFiles, verifyIntegrityManifest } from '../core/integrity.js';
|
|
19
21
|
import { diagnoseJudge, stopJudgeSessionBrokers } from '../core/judge-doctor.js';
|
|
20
22
|
import { resolveJudgeTransport } from '../core/judge-runtime-detection.js';
|
|
23
|
+
import { notificationConfigIssues } from '../core/notify.js';
|
|
21
24
|
import { listRecoveryCheckpoints } from '../core/recovery/checkpoint.js';
|
|
22
25
|
import { recoveryApprovalSetupNotes, recoveryNotificationConfigured, recoveryNotificationSetupWarning, summarizeRecoveryCheckpointDiagnostics, } from '../core/recovery/operator-guidance.js';
|
|
26
|
+
import { inspectRepoConfigTrust } from '../core/repo-config-trust.js';
|
|
23
27
|
import { probeFileCheckpointBackend } from '../core/transactional/backend-selector.js';
|
|
24
28
|
import { fileCheckpointIsolationReason } from '../core/transactional/file-checkpoint-isolation.js';
|
|
25
29
|
import { probeFileCloneStrategy } from '../core/transactional/file-clone.js';
|
|
@@ -52,6 +56,61 @@ function hasCursorGlobalWorkspaceResolver(runtimeSource) {
|
|
|
52
56
|
runtimeSource.includes('working_directory') &&
|
|
53
57
|
runtimeSource.includes('tool_input'));
|
|
54
58
|
}
|
|
59
|
+
const CURSOR_HOOK_SHIMS = [
|
|
60
|
+
'belay-before-submit.mjs',
|
|
61
|
+
'belay-shell-gate.mjs',
|
|
62
|
+
'belay-tool-gate.mjs',
|
|
63
|
+
'belay-audit.mjs',
|
|
64
|
+
];
|
|
65
|
+
function runnerFileName(adapterName, platform) {
|
|
66
|
+
if (platform !== 'win32') {
|
|
67
|
+
return 'belay-runner';
|
|
68
|
+
}
|
|
69
|
+
return adapterName === 'cursor' ? 'belay-runner.ps1' : 'belay-runner.cmd';
|
|
70
|
+
}
|
|
71
|
+
async function cursorOriginIssues(hooksDir, installScope, repoRoot) {
|
|
72
|
+
const canonicalRepoRoot = realpathSync(repoRoot);
|
|
73
|
+
const issues = [];
|
|
74
|
+
for (const fileName of CURSOR_HOOK_SHIMS) {
|
|
75
|
+
const shimPath = path.join(hooksDir, fileName);
|
|
76
|
+
if (!existsSync(shimPath)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
let source;
|
|
80
|
+
try {
|
|
81
|
+
source = await readFile(shimPath, 'utf8');
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
85
|
+
issues.push(`Unable to inspect Cursor hook shim for intended ${installScope} owner: ${shimPath}: ${detail}. Run belay upgrade --scope ${installScope}.`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (!source.includes("from '../belay/runtime/dispatcher.mjs'")) {
|
|
89
|
+
issues.push(`Cursor router generation mismatch for intended ${installScope} owner: ${shimPath}. Run belay upgrade --scope ${installScope}.`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const originMatch = source.match(/origin:\s*(\{[^\n]+\})/);
|
|
93
|
+
let originMatches = false;
|
|
94
|
+
if (originMatch?.[1]) {
|
|
95
|
+
try {
|
|
96
|
+
const origin = JSON.parse(originMatch[1]);
|
|
97
|
+
originMatches =
|
|
98
|
+
installScope === 'global'
|
|
99
|
+
? origin.scope === 'global'
|
|
100
|
+
: origin.scope === 'project' &&
|
|
101
|
+
typeof origin.repoRoot === 'string' &&
|
|
102
|
+
realpathSync(origin.repoRoot) === canonicalRepoRoot;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
originMatches = false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!originMatches) {
|
|
109
|
+
issues.push(`Cursor hook origin mismatch for intended ${installScope} owner: ${shimPath}. Run belay upgrade --scope ${installScope}.`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return issues;
|
|
113
|
+
}
|
|
55
114
|
export async function doctorProject(options = {}) {
|
|
56
115
|
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
57
116
|
const issues = [];
|
|
@@ -76,12 +135,17 @@ export async function doctorProject(options = {}) {
|
|
|
76
135
|
configPath = activeLayout.configPath(repoRoot);
|
|
77
136
|
hooksPath = activeLayout.hooksSettingsPath(repoRoot);
|
|
78
137
|
corePath = path.join(activeLayout.runtimeDir(repoRoot), 'core.mjs');
|
|
138
|
+
const trust = await inspectRepoConfigTrust(repoRoot, adapterName, rawConfig);
|
|
139
|
+
if (!trust.trusted) {
|
|
140
|
+
issues.push(`Repository config is not trusted (${trust.reason}) at ${trust.recordPath}. Review it, then run belay config trust.`);
|
|
141
|
+
}
|
|
79
142
|
if (rawConfig.version === undefined) {
|
|
80
143
|
warnings.push('Config is missing "version". Set "version": 3 explicitly to avoid ambiguous migration.');
|
|
81
144
|
}
|
|
82
145
|
const layered = await loadLayeredConfig(repoRoot, adapterName);
|
|
83
146
|
loadedConfig = layered.config;
|
|
84
147
|
configProvenance = layered.provenance;
|
|
148
|
+
issues.push(...notificationConfigIssues(loadedConfig.notifications, repoRoot));
|
|
85
149
|
for (const entry of layered.provenance) {
|
|
86
150
|
notes.push(`Config layer [${entry.source}]: ${entry.path}`);
|
|
87
151
|
}
|
|
@@ -145,9 +209,9 @@ export async function doctorProject(options = {}) {
|
|
|
145
209
|
}
|
|
146
210
|
if (loadedConfig.controlPlane.integrity === 'hash-pinned') {
|
|
147
211
|
notes.push('Integrity: hash-pinned (verify with belay upgrade after runtime changes).');
|
|
148
|
-
const integrity = await verifyIntegrityManifest(repoRoot, activeLayout);
|
|
212
|
+
const integrity = await verifyIntegrityManifest(repoRoot, activeLayout, runtimeIntegrityFiles(activeLayout, scopedPaths));
|
|
149
213
|
if (!integrity.ok) {
|
|
150
|
-
issues.push(`Integrity verification failed: ${integrity.mismatches.
|
|
214
|
+
issues.push(`Integrity verification failed: ${integrity.mismatches.join(', ')}`);
|
|
151
215
|
}
|
|
152
216
|
}
|
|
153
217
|
}
|
|
@@ -163,14 +227,16 @@ export async function doctorProject(options = {}) {
|
|
|
163
227
|
const repoLocalDir = loadedConfig
|
|
164
228
|
? repoLocalStateDirFor(repoRoot, loadedConfig)
|
|
165
229
|
: activeLayout.repoLocalStateDir(repoRoot);
|
|
166
|
-
const
|
|
167
|
-
path.join(hooksDir,
|
|
168
|
-
path.join(hooksDir, 'belay-runner.cmd'),
|
|
230
|
+
const routingOwnerPaths = [
|
|
231
|
+
path.join(hooksDir, runnerFileName(adapterName, process.platform)),
|
|
169
232
|
path.join(hooksDir, 'belay-before-submit.mjs'),
|
|
170
233
|
path.join(hooksDir, 'belay-shell-gate.mjs'),
|
|
171
234
|
path.join(hooksDir, 'belay-tool-gate.mjs'),
|
|
172
235
|
path.join(hooksDir, 'belay-audit.mjs'),
|
|
236
|
+
...(adapterName === 'cursor' ? [path.join(scopedPaths.runtimeDir, 'dispatcher.mjs')] : []),
|
|
173
237
|
corePath,
|
|
238
|
+
];
|
|
239
|
+
const operationalPaths = [
|
|
174
240
|
loadedConfig
|
|
175
241
|
? pendingApprovalsPath(repoRoot, loadedConfig)
|
|
176
242
|
: path.join(repoLocalDir, 'pending-approvals.json'),
|
|
@@ -179,9 +245,67 @@ export async function doctorProject(options = {}) {
|
|
|
179
245
|
: path.join(repoLocalDir, 'approved-approvals.json'),
|
|
180
246
|
path.join(repoRoot, loadedConfig?.audit.logPath ?? activeLayout.defaultAuditLogPath(repoRoot)),
|
|
181
247
|
];
|
|
248
|
+
const requiredPaths = [...routingOwnerPaths, ...operationalPaths];
|
|
182
249
|
for (const requiredPath of requiredPaths) {
|
|
183
250
|
if (!existsSync(requiredPath)) {
|
|
184
251
|
issues.push(`Missing generated file: ${requiredPath}`);
|
|
252
|
+
if (adapterName === 'cursor' && routingOwnerPaths.includes(requiredPath)) {
|
|
253
|
+
issues.push(`Cursor intended ${installScope} owner is incomplete: missing ${requiredPath}. Run belay upgrade --scope ${installScope}.`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (adapterName === 'cursor') {
|
|
258
|
+
issues.push(...(await cursorOriginIssues(hooksDir, installScope, repoRoot)));
|
|
259
|
+
const dispatcherPath = path.join(scopedPaths.runtimeDir, 'dispatcher.mjs');
|
|
260
|
+
if (existsSync(dispatcherPath) &&
|
|
261
|
+
!hasCurrentCursorDispatcherGeneration(await readFile(dispatcherPath, 'utf8'))) {
|
|
262
|
+
issues.push(`Cursor router generation mismatch for intended ${installScope} owner dispatcher: ${dispatcherPath}. Run belay upgrade --scope ${installScope}.`);
|
|
263
|
+
}
|
|
264
|
+
if (installScope === 'project') {
|
|
265
|
+
const globalPaths = resolveScopedPaths(activeLayout, 'global', repoRoot);
|
|
266
|
+
if (existsSync(globalPaths.hooksSettingsPath)) {
|
|
267
|
+
try {
|
|
268
|
+
const { loadHooksFile } = await import('../installer.js');
|
|
269
|
+
const globalHooks = await loadHooksFile(globalPaths.hooksSettingsPath);
|
|
270
|
+
if (hasManagedCursorHookEntries(globalHooks, process.platform, globalPaths.hooksDir, repoRoot)) {
|
|
271
|
+
const globalOwnerFiles = [
|
|
272
|
+
path.join(globalPaths.hooksDir, runnerFileName('cursor', process.platform)),
|
|
273
|
+
path.join(globalPaths.runtimeDir, 'core.mjs'),
|
|
274
|
+
path.join(globalPaths.runtimeDir, 'dispatcher.mjs'),
|
|
275
|
+
...CURSOR_HOOK_SHIMS.map((fileName) => path.join(globalPaths.hooksDir, fileName)),
|
|
276
|
+
];
|
|
277
|
+
const globalOriginProblems = await cursorOriginIssues(globalPaths.hooksDir, 'global', repoRoot);
|
|
278
|
+
issues.push(...globalOriginProblems);
|
|
279
|
+
const globalProblems = [
|
|
280
|
+
...globalOwnerFiles.filter((filePath) => !existsSync(filePath)),
|
|
281
|
+
...globalOriginProblems,
|
|
282
|
+
];
|
|
283
|
+
const globalDispatcherPath = path.join(globalPaths.runtimeDir, 'dispatcher.mjs');
|
|
284
|
+
if (existsSync(globalDispatcherPath)) {
|
|
285
|
+
try {
|
|
286
|
+
if (!hasCurrentCursorDispatcherGeneration(await readFile(globalDispatcherPath, 'utf8'))) {
|
|
287
|
+
globalProblems.push(globalDispatcherPath);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
292
|
+
issues.push(`Unable to inspect shadowed global Cursor dispatcher at ${globalDispatcherPath}: ${detail}.`);
|
|
293
|
+
globalProblems.push(globalDispatcherPath);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (globalProblems.length > 0) {
|
|
297
|
+
issues.push('Global Cursor installation cannot yield to the project owner. Run belay upgrade from this project to refresh the global router.');
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
notes.push('Healthy global Cursor install is shadowed by this project owner and will return neutral for this repository.');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
306
|
+
issues.push(`Unable to inspect shadowed global Cursor hooks at ${globalPaths.hooksSettingsPath}: ${detail}.`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
185
309
|
}
|
|
186
310
|
}
|
|
187
311
|
let hooksOk = true;
|
|
@@ -196,15 +320,17 @@ export async function doctorProject(options = {}) {
|
|
|
196
320
|
const hooksFile = await loadHooksFile(hooksPath);
|
|
197
321
|
for (const { event, definition } of managedEntries) {
|
|
198
322
|
const entries = hooksFile.hooks[event] ?? [];
|
|
199
|
-
const
|
|
200
|
-
if (!
|
|
323
|
+
const matchingEntry = entries.find((entry) => entry.command === definition.command && entry.matcher === definition.matcher);
|
|
324
|
+
if (!matchingEntry || matchingEntry.failClosed !== true) {
|
|
201
325
|
hooksOk = false;
|
|
202
326
|
const matcherSuffix = definition.matcher ? ` (matcher: ${definition.matcher})` : '';
|
|
203
|
-
issues.push(
|
|
327
|
+
issues.push(matchingEntry
|
|
328
|
+
? `Managed Cursor hook for ${event} must set failClosed: true: ${definition.command}${matcherSuffix}`
|
|
329
|
+
: `Missing managed hook for ${event}: ${definition.command}${matcherSuffix}`);
|
|
204
330
|
}
|
|
205
331
|
}
|
|
206
|
-
if (
|
|
207
|
-
warnings.push('
|
|
332
|
+
if (hasLegacyCursorDoubleShellGates(hooksFile, process.platform, hooksDir, repoRoot)) {
|
|
333
|
+
warnings.push('Legacy Cursor Shell preToolUse gate detected alongside beforeShellExecution. Run belay upgrade to remove the managed Shell matcher.');
|
|
208
334
|
}
|
|
209
335
|
}
|
|
210
336
|
else if (adapterName === 'codex') {
|
|
@@ -212,7 +338,7 @@ export async function doctorProject(options = {}) {
|
|
|
212
338
|
// are present in the rendered TOML block.
|
|
213
339
|
const toml = await readFile(hooksPath, 'utf8');
|
|
214
340
|
for (const { event, definition } of managedEntries) {
|
|
215
|
-
if (!toml
|
|
341
|
+
if (!codexHooksTomlIncludesCommand(toml, definition.command)) {
|
|
216
342
|
hooksOk = false;
|
|
217
343
|
issues.push(`Missing Codex managed hook for ${event}: ${definition.command}`);
|
|
218
344
|
}
|
|
@@ -276,7 +402,7 @@ export async function doctorProject(options = {}) {
|
|
|
276
402
|
if (hasForbiddenShellOverrideLists(loadedConfig)) {
|
|
277
403
|
if (options.dryRun !== true) {
|
|
278
404
|
const stripped = stripForbiddenShellOverrideLists(loadedConfig);
|
|
279
|
-
await
|
|
405
|
+
await writeTrustedConfigFile(repoRoot, stripped, adapterName);
|
|
280
406
|
loadedConfig = stripped;
|
|
281
407
|
notes.push('Removed forbidden legacy shell override lists (overrides.allow / overrides.external).');
|
|
282
408
|
for (let index = issues.length - 1; index >= 0; index -= 1) {
|
|
@@ -346,6 +472,12 @@ export async function doctorProject(options = {}) {
|
|
|
346
472
|
};
|
|
347
473
|
if (dogfood.active) {
|
|
348
474
|
notes.push(`Dogfood active cohort: ${dogfood.gateEvents} gate events, ${dogfood.wouldBlockCount} would-block (${(dogfood.wouldBlockRate * 100).toFixed(1)}%); ${dogfood.excludedGateEvents} historical/mismatched event(s) excluded.`);
|
|
475
|
+
notes.push(`Host denied after Belay allow (cohort): ${auditVisibility.hostDeniedAfterAllowCount}.`);
|
|
476
|
+
warnings.push(...(await detectUndogfoodedLinkedWorktrees({
|
|
477
|
+
repoRoot,
|
|
478
|
+
adapterName,
|
|
479
|
+
layout: activeLayout,
|
|
480
|
+
})));
|
|
349
481
|
if (dogfood.readyForEnforce) {
|
|
350
482
|
notes.push('Dogfood metrics suggest enforce mode is ready (belay dogfood --enforce).');
|
|
351
483
|
}
|