@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
|
@@ -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 };
|
|
@@ -51,16 +51,21 @@ async function managedHooksPresent(adapter, hooksPath, hooksDir, repoRoot) {
|
|
|
51
51
|
/** Cursor modes confirmed not to apply host-level denial after Belay allows. */
|
|
52
52
|
const CURSOR_APPROVAL_MODES_WITHOUT_HOST_DENIAL = new Set(['unrestricted']);
|
|
53
53
|
export function cursorCliConfigPaths(homeDir, env) {
|
|
54
|
+
const candidates = [];
|
|
55
|
+
const addCandidate = (candidate) => {
|
|
56
|
+
if (!candidates.includes(candidate)) {
|
|
57
|
+
candidates.push(candidate);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
54
60
|
const cursorConfigDir = env.CURSOR_CONFIG_DIR?.trim();
|
|
55
61
|
if (cursorConfigDir) {
|
|
56
|
-
|
|
62
|
+
addCandidate(path.join(cursorConfigDir, 'cli-config.json'));
|
|
57
63
|
}
|
|
58
|
-
const candidates = [];
|
|
59
64
|
const xdgConfigHome = env.XDG_CONFIG_HOME?.trim();
|
|
60
65
|
if (xdgConfigHome) {
|
|
61
|
-
|
|
66
|
+
addCandidate(path.join(xdgConfigHome, 'cursor', 'cli-config.json'));
|
|
62
67
|
}
|
|
63
|
-
|
|
68
|
+
addCandidate(path.join(homeDir, '.cursor', 'cli-config.json'));
|
|
64
69
|
return candidates;
|
|
65
70
|
}
|
|
66
71
|
export function cursorApprovalModeRequiresHostDenialWarning(approvalMode) {
|
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',
|
|
@@ -86,7 +86,7 @@ export declare function recoveryCheckpointCommand(options: {
|
|
|
86
86
|
state: import("../core/recovery/types.js").RecoveryCheckpointStateV1;
|
|
87
87
|
manifestHash: string;
|
|
88
88
|
receipt: import("../core/index.js").RecoveryReceiptV1 | undefined;
|
|
89
|
-
repoRoot?: undefined;
|
|
89
|
+
readonly repoRoot?: undefined;
|
|
90
90
|
backend?: undefined;
|
|
91
91
|
availableBackends?: undefined;
|
|
92
92
|
backendCounts?: undefined;
|
|
@@ -118,7 +118,7 @@ export declare function recoveryCheckpointCommand(options: {
|
|
|
118
118
|
paths: string[];
|
|
119
119
|
auditRecorded: boolean;
|
|
120
120
|
message: string;
|
|
121
|
-
repoRoot?: undefined;
|
|
121
|
+
readonly repoRoot?: undefined;
|
|
122
122
|
backend?: undefined;
|
|
123
123
|
availableBackends?: undefined;
|
|
124
124
|
backendCounts?: undefined;
|
|
@@ -146,7 +146,7 @@ export declare function recoveryCheckpointCommand(options: {
|
|
|
146
146
|
subcommand: string;
|
|
147
147
|
verdict: string;
|
|
148
148
|
checkpointId: string;
|
|
149
|
-
repoRoot?: undefined;
|
|
149
|
+
readonly repoRoot?: undefined;
|
|
150
150
|
backend?: undefined;
|
|
151
151
|
availableBackends?: undefined;
|
|
152
152
|
backendCounts?: undefined;
|
|
@@ -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';
|
|
@@ -209,8 +210,23 @@ export async function loadConfigFile(repoRoot, adapter = detectAdapterName(repoR
|
|
|
209
210
|
}
|
|
210
211
|
export async function writeConfigFile(repoRoot, config, adapter = resolveAdapterName(config)) {
|
|
211
212
|
const configPath = configPathFor(repoRoot, adapter);
|
|
212
|
-
|
|
213
|
-
|
|
213
|
+
const directory = path.dirname(configPath);
|
|
214
|
+
const temporaryPath = path.join(directory, `.${path.basename(configPath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
215
|
+
const content = `${JSON.stringify(stripForbiddenShellOverrideLists(config), null, 2)}\n`;
|
|
216
|
+
await mkdir(directory, { recursive: true });
|
|
217
|
+
try {
|
|
218
|
+
await writeFile(temporaryPath, content, { encoding: 'utf8', flag: 'wx' });
|
|
219
|
+
await rename(temporaryPath, configPath);
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
await unlink(temporaryPath).catch(() => undefined);
|
|
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);
|
|
214
230
|
}
|
|
215
231
|
export async function mergeAndWriteConfig(repoRoot, adapter = 'cursor') {
|
|
216
232
|
const layout = getAdapterLayout(adapter);
|
|
@@ -220,7 +236,7 @@ export async function mergeAndWriteConfig(repoRoot, adapter = 'cursor') {
|
|
|
220
236
|
existing = JSON.parse(await readFile(configPath, 'utf8'));
|
|
221
237
|
}
|
|
222
238
|
const merged = mergeConfig(existing, layout.defaultConfig(repoRoot));
|
|
223
|
-
await
|
|
239
|
+
await writeTrustedConfigFile(repoRoot, merged, adapter);
|
|
224
240
|
await ensureBelayStateDir(merged, repoRoot);
|
|
225
241
|
if (merged.controlPlane.enabled) {
|
|
226
242
|
await migrateRepoLocalApprovalsToControlPlane(repoRoot, merged);
|
|
@@ -1,43 +1,61 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import {
|
|
1
|
+
import { createReadStream, existsSync } from 'node:fs';
|
|
2
|
+
import { rename } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
4
5
|
const LEGACY_PLACEHOLDER_PATTERNS = [
|
|
5
6
|
/"(?:timestamp|ts)"\s*:\s*"<timestamp>"/,
|
|
6
7
|
/"(?:fingerprint|commandFingerprint|effectIRHash|payloadHash|configFingerprint|runtimeArtifactHash|decisionConfigFingerprint|receiptHash)"\s*:\s*"<high-entropy>"/,
|
|
7
8
|
/"approvalId"\s*:\s*"<approval-id>"/,
|
|
8
9
|
];
|
|
9
|
-
const
|
|
10
|
-
|
|
10
|
+
const LEGACY_HIGH_ENTROPY_FIELDS = [
|
|
11
|
+
'fingerprint',
|
|
12
|
+
'commandFingerprint',
|
|
13
|
+
'effectIRHash',
|
|
14
|
+
'payloadHash',
|
|
15
|
+
'configFingerprint',
|
|
16
|
+
'runtimeArtifactHash',
|
|
17
|
+
'decisionConfigFingerprint',
|
|
18
|
+
'receiptHash',
|
|
19
|
+
];
|
|
11
20
|
export function auditLogHasLegacyScrubPlaceholders(sample) {
|
|
12
|
-
|
|
21
|
+
for (const line of sample.split(/\r?\n/)) {
|
|
22
|
+
const trimmed = line.trim();
|
|
23
|
+
if (!trimmed) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(trimmed);
|
|
28
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
29
|
+
if (auditRecordHasLegacyCorrelationPlaceholders(parsed)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Malformed legacy lines still need archival before a new runtime appends to the log.
|
|
37
|
+
}
|
|
38
|
+
if (LEGACY_PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(trimmed))) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
13
43
|
}
|
|
14
44
|
export function auditRecordHasLegacyCorrelationPlaceholders(record) {
|
|
15
45
|
return (record.timestamp === '<timestamp>' ||
|
|
16
|
-
record.
|
|
46
|
+
record.ts === '<timestamp>' ||
|
|
47
|
+
LEGACY_HIGH_ENTROPY_FIELDS.some((field) => record[field] === '<high-entropy>') ||
|
|
17
48
|
record.approvalId === '<approval-id>');
|
|
18
49
|
}
|
|
19
50
|
async function auditFileHasLegacyScrubPlaceholders(auditPath) {
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
while (true) {
|
|
26
|
-
const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
|
|
27
|
-
if (bytesRead === 0) {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
const sample = carry + buffer.toString('utf8', 0, bytesRead);
|
|
31
|
-
if (auditLogHasLegacyScrubPlaceholders(sample)) {
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
carry = sample.slice(-AUDIT_SCAN_OVERLAP_CHARS);
|
|
35
|
-
position += bytesRead;
|
|
51
|
+
const input = createReadStream(auditPath, { encoding: 'utf8' });
|
|
52
|
+
const lines = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
|
|
53
|
+
for await (const line of lines) {
|
|
54
|
+
if (auditLogHasLegacyScrubPlaceholders(line)) {
|
|
55
|
+
return true;
|
|
36
56
|
}
|
|
37
57
|
}
|
|
38
|
-
|
|
39
|
-
await handle.close();
|
|
40
|
-
}
|
|
58
|
+
return false;
|
|
41
59
|
}
|
|
42
60
|
export async function archiveLegacyAuditLogIfNeeded(repoRoot, config) {
|
|
43
61
|
const auditPath = path.isAbsolute(config.audit.logPath)
|
|
@@ -5,6 +5,7 @@ import { evaluateFileMutationPolicy, policyDecisionRequiresAsk, } from './capabi
|
|
|
5
5
|
import { canonicalStringify, toolFingerprint } from './fingerprint.js';
|
|
6
6
|
import { matchesSensitivePath } from './glob.js';
|
|
7
7
|
import { pathWithinRoot, resolveWorkspaceRootMatch } from './path-utils.js';
|
|
8
|
+
import { fingerprintToolInputSource } from './replay-scrub.js';
|
|
8
9
|
import { scrubValue } from './scrub.js';
|
|
9
10
|
import { classifyShell, resolveClassifierTrustedCwd } from './verdict/adapter.js';
|
|
10
11
|
import { isGitPath } from './verdict/containment.js';
|
|
@@ -37,6 +38,13 @@ function policyAuth(options) {
|
|
|
37
38
|
function scrubPayload(value, options) {
|
|
38
39
|
return scrubValue(value, options.scrubOptions);
|
|
39
40
|
}
|
|
41
|
+
function fingerprintPayload(payload, options) {
|
|
42
|
+
const toolInput = fingerprintToolInputSource(payload);
|
|
43
|
+
if (Object.keys(toolInput).length > 0) {
|
|
44
|
+
return toolInput;
|
|
45
|
+
}
|
|
46
|
+
return scrubPayload(payload.tool_input ?? {}, options);
|
|
47
|
+
}
|
|
40
48
|
function extractFilePath(payload) {
|
|
41
49
|
const toolInput = payload.tool_input;
|
|
42
50
|
if (!toolInput || typeof toolInput !== 'object') {
|
|
@@ -209,7 +217,7 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
|
|
|
209
217
|
verdict: 'deny_pending_approval',
|
|
210
218
|
reason: 'tool_shell_missing_command',
|
|
211
219
|
summary: canonicalStringify(scrubPayload(payload.tool_input ?? {}, options)),
|
|
212
|
-
fingerprint: toolFingerprint(toolName,
|
|
220
|
+
fingerprint: toolFingerprint(toolName, fingerprintPayload(payload, options), repoRoot),
|
|
213
221
|
assessment: {
|
|
214
222
|
reversibility: 'irreversible',
|
|
215
223
|
external: false,
|
|
@@ -223,7 +231,7 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
|
|
|
223
231
|
verdict: 'allow_flagged',
|
|
224
232
|
reason: 'tool_shell_missing_command',
|
|
225
233
|
summary: canonicalStringify(scrubPayload(payload.tool_input ?? {}, options)),
|
|
226
|
-
fingerprint: toolFingerprint(toolName,
|
|
234
|
+
fingerprint: toolFingerprint(toolName, fingerprintPayload(payload, options), repoRoot),
|
|
227
235
|
assessment: {
|
|
228
236
|
reversibility: 'recoverable_with_cost',
|
|
229
237
|
external: false,
|
|
@@ -249,7 +257,7 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
|
|
|
249
257
|
verdict: 'deny_pending_approval',
|
|
250
258
|
reason: 'file_mutation_missing_path',
|
|
251
259
|
summary: canonicalStringify(scrubPayload(payload.tool_input ?? {}, options)),
|
|
252
|
-
fingerprint: toolFingerprint(toolName,
|
|
260
|
+
fingerprint: toolFingerprint(toolName, fingerprintPayload(payload, options), repoRoot),
|
|
253
261
|
assessment: {
|
|
254
262
|
reversibility: 'irreversible',
|
|
255
263
|
external: false,
|
|
@@ -263,7 +271,7 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
|
|
|
263
271
|
verdict: 'allow_flagged',
|
|
264
272
|
reason: 'file_mutation_missing_path',
|
|
265
273
|
summary: canonicalStringify(scrubPayload(payload.tool_input ?? {}, options)),
|
|
266
|
-
fingerprint: toolFingerprint(toolName,
|
|
274
|
+
fingerprint: toolFingerprint(toolName, fingerprintPayload(payload, options), repoRoot),
|
|
267
275
|
assessment: {
|
|
268
276
|
reversibility: 'recoverable_with_cost',
|
|
269
277
|
external: false,
|
|
@@ -409,7 +417,7 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
|
|
|
409
417
|
verdict: 'deny_pending_approval',
|
|
410
418
|
reason: 'apply_patch_missing_path',
|
|
411
419
|
summary: canonicalStringify(scrubPayload(payload.tool_input ?? {}, options)),
|
|
412
|
-
fingerprint: toolFingerprint(toolName,
|
|
420
|
+
fingerprint: toolFingerprint(toolName, fingerprintPayload(payload, options), repoRoot),
|
|
413
421
|
assessment: {
|
|
414
422
|
reversibility: 'irreversible',
|
|
415
423
|
external: false,
|
|
@@ -423,7 +431,7 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
|
|
|
423
431
|
verdict: 'allow_flagged',
|
|
424
432
|
reason: 'apply_patch_missing_path',
|
|
425
433
|
summary: canonicalStringify(scrubPayload(payload.tool_input ?? {}, options)),
|
|
426
|
-
fingerprint: toolFingerprint(toolName,
|
|
434
|
+
fingerprint: toolFingerprint(toolName, fingerprintPayload(payload, options), repoRoot),
|
|
427
435
|
assessment: {
|
|
428
436
|
reversibility: 'recoverable_with_cost',
|
|
429
437
|
external: false,
|
|
@@ -448,7 +456,7 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
|
|
|
448
456
|
verdict: 'allow_flagged',
|
|
449
457
|
reason: sawDelete ? 'file_delete' : 'file_mutation',
|
|
450
458
|
summary: targets.map((target) => target.path).join(', '),
|
|
451
|
-
fingerprint: toolFingerprint(toolName,
|
|
459
|
+
fingerprint: toolFingerprint(toolName, fingerprintPayload(payload, options), repoRoot),
|
|
452
460
|
assessment: {
|
|
453
461
|
reversibility: 'recoverable_with_cost',
|
|
454
462
|
external: false,
|
|
@@ -462,7 +470,7 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
|
|
|
462
470
|
verdict: 'allow',
|
|
463
471
|
reason: 'unclassified_tool',
|
|
464
472
|
summary: canonicalStringify(scrubPayload(payload.tool_input ?? {}, options)),
|
|
465
|
-
fingerprint: toolFingerprint(toolName,
|
|
473
|
+
fingerprint: toolFingerprint(toolName, fingerprintPayload(payload, options), repoRoot),
|
|
466
474
|
assessment: {
|
|
467
475
|
reversibility: 'reversible',
|
|
468
476
|
external: false,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { getAdapterLayout } from '../adapters/layouts/index.js';
|
|
2
|
+
import type { AdapterName } from '../types.js';
|
|
3
|
+
export declare function listLinkedWorktreePaths(repoRoot: string): Promise<string[]>;
|
|
4
|
+
export declare function detectUndogfoodedLinkedWorktrees(params: {
|
|
5
|
+
repoRoot: string;
|
|
6
|
+
adapterName: AdapterName;
|
|
7
|
+
layout: ReturnType<typeof getAdapterLayout>;
|
|
8
|
+
}): Promise<string[]>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { loadLayeredConfig } from '../config-io.js';
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
export async function listLinkedWorktreePaths(repoRoot) {
|
|
8
|
+
try {
|
|
9
|
+
const { stdout } = await execFileAsync('git', ['worktree', 'list', '--porcelain'], {
|
|
10
|
+
cwd: repoRoot,
|
|
11
|
+
encoding: 'utf8',
|
|
12
|
+
});
|
|
13
|
+
return stdout
|
|
14
|
+
.split(/\r?\n/)
|
|
15
|
+
.filter((line) => line.startsWith('worktree '))
|
|
16
|
+
.map((line) => line.slice('worktree '.length).trim())
|
|
17
|
+
.filter((entry) => entry.length > 0);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function detectUndogfoodedLinkedWorktrees(params) {
|
|
24
|
+
const repoRootCanonical = realpathSync(params.repoRoot);
|
|
25
|
+
const worktrees = await listLinkedWorktreePaths(params.repoRoot);
|
|
26
|
+
const warnings = [];
|
|
27
|
+
for (const worktreePath of worktrees) {
|
|
28
|
+
let canonicalPath = worktreePath;
|
|
29
|
+
try {
|
|
30
|
+
canonicalPath = realpathSync(worktreePath);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Keep the original path for warning output when the entry is stale.
|
|
34
|
+
}
|
|
35
|
+
if (canonicalPath === repoRootCanonical) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const configPath = params.layout.configPath(worktreePath);
|
|
39
|
+
if (!existsSync(configPath)) {
|
|
40
|
+
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.`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const candidate = await loadLayeredConfig(worktreePath, params.adapterName);
|
|
45
|
+
const dogfoodEnabled = candidate.config.mode === 'audit' && candidate.config.policy.unknownLocalEffect === 'deny';
|
|
46
|
+
if (!dogfoodEnabled) {
|
|
47
|
+
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.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
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.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return warnings;
|
|
55
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface ArgvDelegatePeelResult {
|
|
2
|
+
launcher: string;
|
|
3
|
+
innerTokens: string[];
|
|
4
|
+
opaque: boolean;
|
|
5
|
+
reason: string;
|
|
6
|
+
signals: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function innerRecipeFromArgvDelegate(peel: ArgvDelegatePeelResult): string | null;
|
|
9
|
+
export declare function peelArgvDelegateArgv(tokens: string[]): ArgvDelegatePeelResult | null;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export function innerRecipeFromArgvDelegate(peel) {
|
|
2
|
+
if (peel.opaque || peel.innerTokens.length === 0) {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
return peel.innerTokens.join(' ');
|
|
6
|
+
}
|
|
7
|
+
export function peelArgvDelegateArgv(tokens) {
|
|
8
|
+
const launcher = tokens[0] ?? '';
|
|
9
|
+
if (!launcher || tokens.length < 2) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const signals = ['process.argv_delegate'];
|
|
13
|
+
let index = 1;
|
|
14
|
+
while (index < tokens.length) {
|
|
15
|
+
const token = tokens[index] ?? '';
|
|
16
|
+
if (token === '--') {
|
|
17
|
+
index += 1;
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
if (token.startsWith('-')) {
|
|
21
|
+
return {
|
|
22
|
+
launcher,
|
|
23
|
+
innerTokens: [],
|
|
24
|
+
opaque: true,
|
|
25
|
+
reason: 'argv_delegate_wrapper_options',
|
|
26
|
+
signals: [...signals, 'argv_delegate_wrapper_options'],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
const innerTokens = tokens.slice(index);
|
|
32
|
+
if (innerTokens.length === 0) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
launcher,
|
|
37
|
+
innerTokens,
|
|
38
|
+
opaque: false,
|
|
39
|
+
reason: 'argv_delegate_peel',
|
|
40
|
+
signals,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ShellEffectRequirement } from '../shell-build.js';
|
|
2
|
+
export declare function shouldApplyArgvDelegate(head: string, innerTokens: string[], depth: number): boolean;
|
|
3
|
+
export declare function isGrammarUnknownOnly(requirements: ShellEffectRequirement[], head: string): boolean;
|