@guilz-dev/belay 0.9.4 → 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 +3 -2
- package/dist/adapters/cursor/hook-dispatch-entry.d.ts +3 -0
- package/dist/adapters/cursor/hook-dispatch-entry.js +11 -0
- package/dist/adapters/cursor/hook-router.js +14 -7
- package/dist/adapters/cursor/hooks.d.ts +1 -1
- package/dist/adapters/cursor/hooks.js +16 -5
- 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/runtime-entry.d.ts +2 -2
- package/dist/adapters/cursor/runtime-entry.js +35 -10
- package/dist/adapters/shared/gate-runtime.js +28 -17
- package/dist/bundle/claude-runtime.mjs +602 -438
- package/dist/bundle/codex-runtime.mjs +612 -448
- package/dist/bundle/cursor-dispatcher.mjs +100 -26
- package/dist/bundle/cursor-runtime.mjs +662 -470
- 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 +10 -54
- 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/judge.js +2 -2
- package/dist/commands/recovery-checkpoints.js +2 -11
- package/dist/config-io.d.ts +1 -0
- package/dist/config-io.js +8 -1
- package/dist/core/dogfood-environment.d.ts +8 -0
- package/dist/core/dogfood-environment.js +55 -0
- package/dist/core/effect-ir/shell-lower/decoders/belay.js +12 -0
- package/dist/core/egress-approval.js +0 -18
- 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/repo-config-trust.d.ts +31 -0
- package/dist/core/repo-config-trust.js +152 -0
- package/dist/corpus/benign-probe-cores.d.ts +1 -1
- package/dist/corpus/benign-probe-cores.js +0 -1
- package/dist/defaults.js +0 -25
- package/dist/installer/scope-config.js +2 -2
- package/dist/installer.js +4 -4
- package/dist/types.d.ts +19 -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,8 +1,6 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
2
1
|
import { existsSync, realpathSync } from 'node:fs';
|
|
3
2
|
import { readFile } from 'node:fs/promises';
|
|
4
3
|
import path from 'node:path';
|
|
5
|
-
import { promisify } from 'node:util';
|
|
6
4
|
import { getClaudeManagedHookEntries } from '../adapters/claude/hooks.js';
|
|
7
5
|
import { codexHooksTomlIncludesCommand, getCodexManagedHookEntries, } from '../adapters/codex/hooks.js';
|
|
8
6
|
import { hasCurrentCursorDispatcherGeneration } from '../adapters/cursor/dispatcher-generation.js';
|
|
@@ -11,18 +9,21 @@ import { getAdapterLayout } from '../adapters/layouts/index.js';
|
|
|
11
9
|
import { protectedArtifactRoots } from '../adapters/layouts/protected-paths.js';
|
|
12
10
|
import { resolveScopedPaths } from '../adapters/layouts/scope.js';
|
|
13
11
|
import { cleanupOrphanApprovalState } from '../cleanup-orphans.js';
|
|
14
|
-
import { approvedApprovalsPath, belayStateDir, detectAdapterName, loadLayeredConfig, pendingApprovalsPath, repoLocalStateDirFor,
|
|
12
|
+
import { approvedApprovalsPath, belayStateDir, detectAdapterName, loadLayeredConfig, pendingApprovalsPath, repoLocalStateDirFor, writeTrustedConfigFile, } from '../config-io.js';
|
|
15
13
|
import { approvalSigningKeyPath } from '../core/approval-token.js';
|
|
16
14
|
import { auditRecordHasLegacyCorrelationPlaceholders } from '../core/audit-legacy-archive.js';
|
|
17
15
|
import { detectFenceDrift, summarizeAuditVisibility } from '../core/audit-summary.js';
|
|
18
16
|
import { inspectBoundaryAttestationFile } from '../core/capability/boundary-attestation-sign.js';
|
|
19
17
|
import { boundaryAttestationPath, boundarySessionStatus, } from '../core/capability/boundary-session.js';
|
|
20
18
|
import { configuredControlPlaneDir, defaultControlPlaneDir, hasForbiddenShellOverrideLists, stripForbiddenShellOverrideLists, } from '../core/config.js';
|
|
19
|
+
import { detectUndogfoodedLinkedWorktrees } from '../core/dogfood-environment.js';
|
|
21
20
|
import { runtimeIntegrityFiles, verifyIntegrityManifest } from '../core/integrity.js';
|
|
22
21
|
import { diagnoseJudge, stopJudgeSessionBrokers } from '../core/judge-doctor.js';
|
|
23
22
|
import { resolveJudgeTransport } from '../core/judge-runtime-detection.js';
|
|
23
|
+
import { notificationConfigIssues } from '../core/notify.js';
|
|
24
24
|
import { listRecoveryCheckpoints } from '../core/recovery/checkpoint.js';
|
|
25
25
|
import { recoveryApprovalSetupNotes, recoveryNotificationConfigured, recoveryNotificationSetupWarning, summarizeRecoveryCheckpointDiagnostics, } from '../core/recovery/operator-guidance.js';
|
|
26
|
+
import { inspectRepoConfigTrust } from '../core/repo-config-trust.js';
|
|
26
27
|
import { probeFileCheckpointBackend } from '../core/transactional/backend-selector.js';
|
|
27
28
|
import { fileCheckpointIsolationReason } from '../core/transactional/file-checkpoint-isolation.js';
|
|
28
29
|
import { probeFileCloneStrategy } from '../core/transactional/file-clone.js';
|
|
@@ -36,7 +37,6 @@ import { PACKAGE_VERSION } from '../version.js';
|
|
|
36
37
|
import { loadAuditRecords } from './audit.js';
|
|
37
38
|
import { collectHealthSnapshot } from './health-snapshot.js';
|
|
38
39
|
import { metricsProject } from './metrics.js';
|
|
39
|
-
const execFileAsync = promisify(execFile);
|
|
40
40
|
function resolveDoctorAdapter(options, configAdapter) {
|
|
41
41
|
if (options.adapter) {
|
|
42
42
|
return options.adapter;
|
|
@@ -111,55 +111,6 @@ async function cursorOriginIssues(hooksDir, installScope, repoRoot) {
|
|
|
111
111
|
}
|
|
112
112
|
return issues;
|
|
113
113
|
}
|
|
114
|
-
async function listLinkedWorktreePaths(repoRoot) {
|
|
115
|
-
try {
|
|
116
|
-
const { stdout } = await execFileAsync('git', ['worktree', 'list', '--porcelain'], {
|
|
117
|
-
cwd: repoRoot,
|
|
118
|
-
encoding: 'utf8',
|
|
119
|
-
});
|
|
120
|
-
return stdout
|
|
121
|
-
.split(/\r?\n/)
|
|
122
|
-
.filter((line) => line.startsWith('worktree '))
|
|
123
|
-
.map((line) => line.slice('worktree '.length).trim())
|
|
124
|
-
.filter((entry) => entry.length > 0);
|
|
125
|
-
}
|
|
126
|
-
catch {
|
|
127
|
-
return [];
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
async function detectUndogfoodedLinkedWorktrees(params) {
|
|
131
|
-
const repoRootCanonical = realpathSync(params.repoRoot);
|
|
132
|
-
const worktrees = await listLinkedWorktreePaths(params.repoRoot);
|
|
133
|
-
const warnings = [];
|
|
134
|
-
for (const worktreePath of worktrees) {
|
|
135
|
-
let canonicalPath = worktreePath;
|
|
136
|
-
try {
|
|
137
|
-
canonicalPath = realpathSync(worktreePath);
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
// Keep the original path for warning output when the entry is stale.
|
|
141
|
-
}
|
|
142
|
-
if (canonicalPath === repoRootCanonical) {
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
const configPath = params.layout.configPath(worktreePath);
|
|
146
|
-
if (!existsSync(configPath)) {
|
|
147
|
-
warnings.push(`Dogfood is active here but ${path.basename(worktreePath)} has no belay.config.json (defaults to enforce). Run belay dogfood in each worktree you use with Cursor.`);
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
try {
|
|
151
|
-
const candidate = await loadLayeredConfig(worktreePath, params.adapterName);
|
|
152
|
-
const dogfoodEnabled = candidate.config.mode === 'audit' && candidate.config.policy.unknownLocalEffect === 'deny';
|
|
153
|
-
if (!dogfoodEnabled) {
|
|
154
|
-
warnings.push(`Dogfood is active here but ${path.basename(worktreePath)} is not in dogfood mode (mode=${candidate.config.mode}, unknownLocalEffect=${candidate.config.policy.unknownLocalEffect}). Run belay dogfood in each worktree you use with Cursor.`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
catch {
|
|
158
|
-
warnings.push(`Dogfood is active here but ${path.basename(worktreePath)} has an unreadable belay.config.json. Run belay doctor and belay dogfood in that worktree.`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return warnings;
|
|
162
|
-
}
|
|
163
114
|
export async function doctorProject(options = {}) {
|
|
164
115
|
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
165
116
|
const issues = [];
|
|
@@ -184,12 +135,17 @@ export async function doctorProject(options = {}) {
|
|
|
184
135
|
configPath = activeLayout.configPath(repoRoot);
|
|
185
136
|
hooksPath = activeLayout.hooksSettingsPath(repoRoot);
|
|
186
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
|
+
}
|
|
187
142
|
if (rawConfig.version === undefined) {
|
|
188
143
|
warnings.push('Config is missing "version". Set "version": 3 explicitly to avoid ambiguous migration.');
|
|
189
144
|
}
|
|
190
145
|
const layered = await loadLayeredConfig(repoRoot, adapterName);
|
|
191
146
|
loadedConfig = layered.config;
|
|
192
147
|
configProvenance = layered.provenance;
|
|
148
|
+
issues.push(...notificationConfigIssues(loadedConfig.notifications, repoRoot));
|
|
193
149
|
for (const entry of layered.provenance) {
|
|
194
150
|
notes.push(`Config layer [${entry.source}]: ${entry.path}`);
|
|
195
151
|
}
|
|
@@ -446,7 +402,7 @@ export async function doctorProject(options = {}) {
|
|
|
446
402
|
if (hasForbiddenShellOverrideLists(loadedConfig)) {
|
|
447
403
|
if (options.dryRun !== true) {
|
|
448
404
|
const stripped = stripForbiddenShellOverrideLists(loadedConfig);
|
|
449
|
-
await
|
|
405
|
+
await writeTrustedConfigFile(repoRoot, stripped, adapterName);
|
|
450
406
|
loadedConfig = stripped;
|
|
451
407
|
notes.push('Removed forbidden legacy shell override lists (overrides.allow / overrides.external).');
|
|
452
408
|
for (let index = issues.length - 1; index >= 0; index -= 1) {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { getAdapterLayout } from '../adapters/layouts/index.js';
|
|
3
|
+
import { detectAdapterName, loadConfigFile } from '../config-io.js';
|
|
4
|
+
import { isGateRecord, parseTimestamp } from '../core/audit-query.js';
|
|
5
|
+
import { summarizeAuditVisibility } from '../core/audit-summary.js';
|
|
6
|
+
import { detectUndogfoodedLinkedWorktrees } from '../core/dogfood-environment.js';
|
|
7
|
+
import { matchesAuditCohort, resolveActiveAuditCohort } from '../runtime-provenance.js';
|
|
8
|
+
import { loadAuditRecords } from './audit.js';
|
|
9
|
+
function isDogfoodMode(config) {
|
|
10
|
+
return config.mode === 'audit' && config.policy?.unknownLocalEffect === 'deny';
|
|
11
|
+
}
|
|
12
|
+
function baseResult(repoRoot, since) {
|
|
13
|
+
return {
|
|
14
|
+
ok: false,
|
|
15
|
+
repoRoot,
|
|
16
|
+
since,
|
|
17
|
+
gateEvents: 0,
|
|
18
|
+
auditModeDenyCount: 0,
|
|
19
|
+
hostDeniedAfterAllowCount: 0,
|
|
20
|
+
shellPreToolUseCount: 0,
|
|
21
|
+
mismatchedCohortCount: 0,
|
|
22
|
+
environmentSkewCount: 0,
|
|
23
|
+
failures: [],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export async function checkDogfoodProject(options) {
|
|
27
|
+
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
28
|
+
const since = options.since;
|
|
29
|
+
const result = baseResult(repoRoot, since);
|
|
30
|
+
const sinceMs = parseTimestamp(since);
|
|
31
|
+
if (sinceMs === null) {
|
|
32
|
+
result.failures.push('invalid_since');
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
const adapter = options.adapter ?? detectAdapterName(repoRoot);
|
|
36
|
+
const config = await loadConfigFile(repoRoot, adapter);
|
|
37
|
+
const dogfoodActive = isDogfoodMode(config);
|
|
38
|
+
if (!dogfoodActive) {
|
|
39
|
+
result.failures.push('dogfood_inactive');
|
|
40
|
+
}
|
|
41
|
+
const records = await loadAuditRecords(repoRoot, adapter);
|
|
42
|
+
const invalidTimestampRecord = records.some((record) => parseTimestamp(record.timestamp) === null);
|
|
43
|
+
if (invalidTimestampRecord) {
|
|
44
|
+
result.failures.push('invalid_timestamp_record');
|
|
45
|
+
}
|
|
46
|
+
const inWindowRecords = records.filter((record) => {
|
|
47
|
+
const recordMs = parseTimestamp(record.timestamp);
|
|
48
|
+
return recordMs !== null && recordMs >= sinceMs;
|
|
49
|
+
});
|
|
50
|
+
const activeCohort = await resolveActiveAuditCohort(repoRoot, config);
|
|
51
|
+
const inWindowGateRecords = inWindowRecords.filter((record) => isGateRecord(record));
|
|
52
|
+
let cohortGateRecords = inWindowGateRecords;
|
|
53
|
+
let cohortScopedRecords = inWindowRecords;
|
|
54
|
+
if (activeCohort) {
|
|
55
|
+
cohortGateRecords = inWindowGateRecords.filter((record) => matchesAuditCohort(record, activeCohort));
|
|
56
|
+
const cohortEventRecords = new Set(cohortGateRecords);
|
|
57
|
+
cohortScopedRecords = inWindowRecords.filter((record) => !isGateRecord(record) || cohortEventRecords.has(record));
|
|
58
|
+
result.mismatchedCohortCount = inWindowGateRecords.length - cohortGateRecords.length;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
result.mismatchedCohortCount = inWindowGateRecords.length;
|
|
62
|
+
if (inWindowGateRecords.length > 0) {
|
|
63
|
+
result.failures.push('active_cohort_unavailable');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
result.gateEvents = cohortGateRecords.length;
|
|
67
|
+
result.auditModeDenyCount = cohortGateRecords.filter((record) => record.mode === 'audit' && record.permission === 'deny').length;
|
|
68
|
+
result.shellPreToolUseCount = cohortGateRecords.filter((record) => record.kind === 'shell' && record.event === 'preToolUse').length;
|
|
69
|
+
result.hostDeniedAfterAllowCount =
|
|
70
|
+
summarizeAuditVisibility(cohortScopedRecords).hostDeniedAfterAllowCount;
|
|
71
|
+
if (result.gateEvents === 0) {
|
|
72
|
+
result.failures.push('no_gate_events_since_cutoff');
|
|
73
|
+
}
|
|
74
|
+
if (result.auditModeDenyCount > 0) {
|
|
75
|
+
result.failures.push('audit_mode_permission_deny');
|
|
76
|
+
}
|
|
77
|
+
if (result.hostDeniedAfterAllowCount > 0) {
|
|
78
|
+
result.failures.push('host_denied_after_allow');
|
|
79
|
+
}
|
|
80
|
+
if (result.shellPreToolUseCount > 0) {
|
|
81
|
+
result.failures.push('shell_event_recorded_as_preToolUse');
|
|
82
|
+
}
|
|
83
|
+
if (result.mismatchedCohortCount > 0) {
|
|
84
|
+
result.failures.push('mismatched_active_cohort');
|
|
85
|
+
}
|
|
86
|
+
if (dogfoodActive) {
|
|
87
|
+
const environmentWarnings = await detectUndogfoodedLinkedWorktrees({
|
|
88
|
+
repoRoot,
|
|
89
|
+
adapterName: adapter,
|
|
90
|
+
layout: getAdapterLayout(adapter),
|
|
91
|
+
});
|
|
92
|
+
result.environmentSkewCount = environmentWarnings.length;
|
|
93
|
+
if (result.environmentSkewCount > 0) {
|
|
94
|
+
result.failures.push('environment_skew');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
result.ok = result.failures.length === 0;
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
export function formatDogfoodCheckResult(result) {
|
|
101
|
+
const lines = [
|
|
102
|
+
`dogfood check for ${result.repoRoot}`,
|
|
103
|
+
`since: ${result.since}`,
|
|
104
|
+
`gate events: ${result.gateEvents}`,
|
|
105
|
+
`audit-mode deny count: ${result.auditModeDenyCount}`,
|
|
106
|
+
`host denied-after-allow count: ${result.hostDeniedAfterAllowCount}`,
|
|
107
|
+
`shell preToolUse count: ${result.shellPreToolUseCount}`,
|
|
108
|
+
`mismatched active cohort count: ${result.mismatchedCohortCount}`,
|
|
109
|
+
`environment skew count: ${result.environmentSkewCount}`,
|
|
110
|
+
`status: ${result.ok ? 'ok' : 'fail'}`,
|
|
111
|
+
];
|
|
112
|
+
if (!result.ok) {
|
|
113
|
+
lines.push(`failures: ${result.failures.join(', ')}`);
|
|
114
|
+
}
|
|
115
|
+
return `${lines.join('\n')}\n`;
|
|
116
|
+
}
|
|
@@ -2,4 +2,5 @@ import { isDogfoodConfig, loadOperationalInsights } from '../operational-insight
|
|
|
2
2
|
import type { DogfoodOptions, DogfoodResult } from '../types.js';
|
|
3
3
|
export declare function dogfoodProject(options?: DogfoodOptions): Promise<DogfoodResult>;
|
|
4
4
|
export declare function formatDogfoodResult(result: DogfoodResult): string;
|
|
5
|
+
export { checkDogfoodProject, formatDogfoodCheckResult } from './dogfood-check.js';
|
|
5
6
|
export { isDogfoodConfig, loadOperationalInsights };
|
package/dist/commands/dogfood.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { configPathFor, loadConfigFile,
|
|
2
|
+
import { configPathFor, loadConfigFile, writeTrustedConfigFile } from '../config-io.js';
|
|
3
3
|
import { mergeConfig } from '../core/config.js';
|
|
4
4
|
import { isDogfoodConfig, loadOperationalInsights } from '../operational-insights.js';
|
|
5
5
|
import { metricsProject } from './metrics.js';
|
|
@@ -19,7 +19,7 @@ export async function dogfoodProject(options = {}) {
|
|
|
19
19
|
unknownLocalEffect: 'deny',
|
|
20
20
|
},
|
|
21
21
|
});
|
|
22
|
-
await
|
|
22
|
+
await writeTrustedConfigFile(repoRoot, updated, adapter);
|
|
23
23
|
return {
|
|
24
24
|
ok: true,
|
|
25
25
|
repoRoot,
|
|
@@ -53,7 +53,7 @@ async function promoteDogfoodToEnforce(repoRoot, configPath, force, adapter = 'c
|
|
|
53
53
|
...existing,
|
|
54
54
|
mode: 'enforce',
|
|
55
55
|
});
|
|
56
|
-
await
|
|
56
|
+
await writeTrustedConfigFile(repoRoot, updated, adapter);
|
|
57
57
|
return {
|
|
58
58
|
ok: true,
|
|
59
59
|
repoRoot,
|
|
@@ -68,4 +68,5 @@ async function promoteDogfoodToEnforce(repoRoot, configPath, force, adapter = 'c
|
|
|
68
68
|
export function formatDogfoodResult(result) {
|
|
69
69
|
return `${result.message}\n`;
|
|
70
70
|
}
|
|
71
|
+
export { checkDogfoodProject, formatDogfoodCheckResult } from './dogfood-check.js';
|
|
71
72
|
export { isDogfoodConfig, loadOperationalInsights };
|
package/dist/commands/judge.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { stdin as input, stdout as output } from 'node:process';
|
|
3
3
|
import { createInterface } from 'node:readline/promises';
|
|
4
|
-
import { configPathFor, loadApprovalState, loadConfigFile, repoLocalStateDirFor, resolveAdapterName,
|
|
4
|
+
import { configPathFor, loadApprovalState, loadConfigFile, repoLocalStateDirFor, resolveAdapterName, writeTrustedConfigFile, } from '../config-io.js';
|
|
5
5
|
import { appendCliAuditEvent } from '../core/audit-io.js';
|
|
6
6
|
import { JUDGE_CLOUD_CONSENT_REASON } from '../core/capability/reasons.js';
|
|
7
7
|
import { belayStateDir, normalizeJudgeConfig } from '../core/config.js';
|
|
@@ -176,7 +176,7 @@ export async function judgeUse(options) {
|
|
|
176
176
|
}
|
|
177
177
|
const after = normalizeJudgeConfig(patch.judge);
|
|
178
178
|
const updated = { ...config, judge: after };
|
|
179
|
-
await
|
|
179
|
+
await writeTrustedConfigFile(repoRoot, updated, adapter);
|
|
180
180
|
await refreshIntegrityIfPinned(repoRoot, updated);
|
|
181
181
|
await appendCliAuditEvent(repoRoot, updated, {
|
|
182
182
|
event: 'judge_provider_changed',
|
|
@@ -5,7 +5,6 @@ import { protectedArtifactRoots } from '../adapters/layouts/protected-paths.js';
|
|
|
5
5
|
import { ensureBelayStateDir, loadConfigFile } from '../config-io.js';
|
|
6
6
|
import { compactApprovals, createApprovalRecordWithEnvelope } from '../core/approval.js';
|
|
7
7
|
import { createGateApprovalStore } from '../core/approval-service.js';
|
|
8
|
-
import { issueApprovalToken } from '../core/approval-token.js';
|
|
9
8
|
import { appendCliAuditEvent } from '../core/audit-io.js';
|
|
10
9
|
import { mutateApprovalStateWithRetry } from '../core/capability/approval-state-mutation.js';
|
|
11
10
|
import { boundarySessionStatus } from '../core/capability/boundary-session.js';
|
|
@@ -253,20 +252,12 @@ export async function recoveryCheckpointCommand(options) {
|
|
|
253
252
|
}
|
|
254
253
|
const notificationConfigured = recoveryNotificationConfigured(config);
|
|
255
254
|
if (notificationConfigured) {
|
|
256
|
-
const approvalToken = await issueApprovalToken({
|
|
257
|
-
approvalId: request.approvalId,
|
|
258
|
-
fingerprint: request.fingerprint,
|
|
259
|
-
repoRoot: request.repoRoot,
|
|
260
|
-
issuedAt: request.createdAt,
|
|
261
|
-
expiresAt: request.expiresAt,
|
|
262
|
-
}, configuredControlPlaneDir(config));
|
|
263
255
|
await notifyDeny(config.notifications, {
|
|
264
256
|
approvalId: request.approvalId,
|
|
265
257
|
reason: request.reason,
|
|
266
258
|
summary: request.summary,
|
|
267
259
|
repoRoot: request.repoRoot,
|
|
268
260
|
fingerprint: request.fingerprint,
|
|
269
|
-
approvalToken,
|
|
270
261
|
});
|
|
271
262
|
}
|
|
272
263
|
const auditRecorded = await appendRecoveryAudit(repoRoot, config, {
|
|
@@ -287,8 +278,8 @@ export async function recoveryCheckpointCommand(options) {
|
|
|
287
278
|
paths: binding.paths,
|
|
288
279
|
auditRecorded,
|
|
289
280
|
message: notificationConfigured
|
|
290
|
-
? `Signed
|
|
291
|
-
: `${recoveryNotificationSetupWarning()} Pending approval id: ${request.approvalId}.`,
|
|
281
|
+
? `Signed approval required for ${request.approvalId}. Run \`belay approval-token ${request.approvalId}\` locally, then \`belay approve ${request.approvalId} --token <signed-token>\`, and repeat this command.`
|
|
282
|
+
: `${recoveryNotificationSetupWarning()} Pending approval id: ${request.approvalId}. Run \`belay approval-token ${request.approvalId}\` locally to retrieve the signed token.`,
|
|
292
283
|
};
|
|
293
284
|
}
|
|
294
285
|
try {
|
package/dist/config-io.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export declare function migrateControlPlaneApprovalsToRepoLocal(repoRoot: string
|
|
|
17
17
|
export declare function loadLayeredConfig(repoRoot: string, adapter?: AdapterName): Promise<LayeredConfigResult>;
|
|
18
18
|
export declare function loadConfigFile(repoRoot: string, adapter?: AdapterName): Promise<BelayConfigV3>;
|
|
19
19
|
export declare function writeConfigFile(repoRoot: string, config: BelayConfigV3, adapter?: AdapterName): Promise<void>;
|
|
20
|
+
export declare function writeTrustedConfigFile(repoRoot: string, config: BelayConfigV3, adapter?: AdapterName): Promise<void>;
|
|
20
21
|
export declare function mergeAndWriteConfig(repoRoot: string, adapter?: AdapterName): Promise<BelayConfigV3>;
|
|
21
22
|
export declare function loadApprovalState(repoRoot: string, fileName: 'pending-approvals.json' | 'approved-approvals.json', config: BelayConfigV3): Promise<ApprovalStateFile>;
|
|
22
23
|
export declare function saveApprovalState(repoRoot: string, fileName: 'pending-approvals.json' | 'approved-approvals.json', state: ApprovalStateFile, config: BelayConfigV3): Promise<void>;
|
package/dist/config-io.js
CHANGED
|
@@ -7,6 +7,7 @@ import { compactApprovals, isExpired, mergeApprovalStates } from './core/approva
|
|
|
7
7
|
import { mutateApprovalStateWithRetry } from './core/capability/approval-state-mutation.js';
|
|
8
8
|
import { approvedApprovalsFile, belayStateDir, configuredControlPlaneDir, mergeConfig, pendingApprovalsFile, stripForbiddenShellOverrideLists, } from './core/config.js';
|
|
9
9
|
import { resolveLayeredConfig, teamConfigPath, } from './core/config-layers.js';
|
|
10
|
+
import { trustRepoConfig } from './core/repo-config-trust.js';
|
|
10
11
|
export function resolveAdapterName(config) {
|
|
11
12
|
if (config.adapter === 'claude') {
|
|
12
13
|
return 'claude';
|
|
@@ -221,6 +222,12 @@ export async function writeConfigFile(repoRoot, config, adapter = resolveAdapter
|
|
|
221
222
|
await unlink(temporaryPath).catch(() => undefined);
|
|
222
223
|
}
|
|
223
224
|
}
|
|
225
|
+
export async function writeTrustedConfigFile(repoRoot, config, adapter = resolveAdapterName(config)) {
|
|
226
|
+
await writeConfigFile(repoRoot, config, adapter);
|
|
227
|
+
const configPath = configPathFor(repoRoot, adapter);
|
|
228
|
+
const parsed = JSON.parse(await readFile(configPath, 'utf8'));
|
|
229
|
+
await trustRepoConfig(repoRoot, adapter, parsed);
|
|
230
|
+
}
|
|
224
231
|
export async function mergeAndWriteConfig(repoRoot, adapter = 'cursor') {
|
|
225
232
|
const layout = getAdapterLayout(adapter);
|
|
226
233
|
const configPath = layout.configPath(repoRoot);
|
|
@@ -229,7 +236,7 @@ export async function mergeAndWriteConfig(repoRoot, adapter = 'cursor') {
|
|
|
229
236
|
existing = JSON.parse(await readFile(configPath, 'utf8'));
|
|
230
237
|
}
|
|
231
238
|
const merged = mergeConfig(existing, layout.defaultConfig(repoRoot));
|
|
232
|
-
await
|
|
239
|
+
await writeTrustedConfigFile(repoRoot, merged, adapter);
|
|
233
240
|
await ensureBelayStateDir(merged, repoRoot);
|
|
234
241
|
if (merged.controlPlane.enabled) {
|
|
235
242
|
await migrateRepoLocalApprovalsToControlPlane(repoRoot, merged);
|