@guilz-dev/belay 0.9.3 → 0.9.4
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 +35 -1
- 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 +7 -0
- package/dist/adapters/cursor/hook-dispatch-entry.js +126 -0
- package/dist/adapters/cursor/hook-router.d.ts +23 -0
- package/dist/adapters/cursor/hook-router.js +266 -0
- package/dist/adapters/cursor/hooks.d.ts +4 -0
- package/dist/adapters/cursor/hooks.js +99 -8
- 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 +39 -39
- package/dist/adapters/layouts/scope.d.ts +5 -0
- package/dist/adapters/layouts/scope.js +71 -0
- package/dist/adapters/shared/gate-runtime.js +1 -0
- package/dist/bundle/claude-runtime.mjs +1932 -1740
- package/dist/bundle/codex-runtime.mjs +1934 -1742
- package/dist/bundle/cursor-dispatcher.mjs +443 -0
- package/dist/bundle/cursor-runtime.mjs +2017 -1821
- package/dist/commands/doctor.js +191 -15
- package/dist/commands/health-snapshot.js +9 -4
- package/dist/commands/recovery-checkpoints.d.ts +3 -3
- package/dist/config-io.js +11 -2
- package/dist/core/audit-legacy-archive.js +43 -25
- package/dist/core/classify-tool.js +16 -8
- 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 +29 -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/glob.js +32 -16
- package/dist/core/integrity.d.ts +2 -2
- package/dist/core/integrity.js +52 -12
- package/dist/core/replay-scrub.d.ts +2 -0
- package/dist/core/replay-scrub.js +8 -0
- package/dist/core/shell-substitution.js +2 -2
- package/dist/core/shell-unparseable.js +3 -1
- package/dist/defaults.d.ts +7 -3
- package/dist/defaults.js +29 -22
- 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.js +44 -9
- 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 +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
2
3
|
import { readFile } from 'node:fs/promises';
|
|
3
4
|
import path from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
4
6
|
import { getClaudeManagedHookEntries } from '../adapters/claude/hooks.js';
|
|
5
|
-
import { getCodexManagedHookEntries } from '../adapters/codex/hooks.js';
|
|
6
|
-
import {
|
|
7
|
+
import { codexHooksTomlIncludesCommand, getCodexManagedHookEntries, } from '../adapters/codex/hooks.js';
|
|
8
|
+
import { hasCurrentCursorDispatcherGeneration } from '../adapters/cursor/dispatcher-generation.js';
|
|
9
|
+
import { hasLegacyCursorDoubleShellGates, hasManagedCursorHookEntries, } from '../adapters/cursor/hooks.js';
|
|
7
10
|
import { getAdapterLayout } from '../adapters/layouts/index.js';
|
|
8
11
|
import { protectedArtifactRoots } from '../adapters/layouts/protected-paths.js';
|
|
9
12
|
import { resolveScopedPaths } from '../adapters/layouts/scope.js';
|
|
@@ -15,7 +18,7 @@ import { detectFenceDrift, summarizeAuditVisibility } from '../core/audit-summar
|
|
|
15
18
|
import { inspectBoundaryAttestationFile } from '../core/capability/boundary-attestation-sign.js';
|
|
16
19
|
import { boundaryAttestationPath, boundarySessionStatus, } from '../core/capability/boundary-session.js';
|
|
17
20
|
import { configuredControlPlaneDir, defaultControlPlaneDir, hasForbiddenShellOverrideLists, stripForbiddenShellOverrideLists, } from '../core/config.js';
|
|
18
|
-
import { verifyIntegrityManifest } from '../core/integrity.js';
|
|
21
|
+
import { runtimeIntegrityFiles, verifyIntegrityManifest } from '../core/integrity.js';
|
|
19
22
|
import { diagnoseJudge, stopJudgeSessionBrokers } from '../core/judge-doctor.js';
|
|
20
23
|
import { resolveJudgeTransport } from '../core/judge-runtime-detection.js';
|
|
21
24
|
import { listRecoveryCheckpoints } from '../core/recovery/checkpoint.js';
|
|
@@ -33,6 +36,7 @@ import { PACKAGE_VERSION } from '../version.js';
|
|
|
33
36
|
import { loadAuditRecords } from './audit.js';
|
|
34
37
|
import { collectHealthSnapshot } from './health-snapshot.js';
|
|
35
38
|
import { metricsProject } from './metrics.js';
|
|
39
|
+
const execFileAsync = promisify(execFile);
|
|
36
40
|
function resolveDoctorAdapter(options, configAdapter) {
|
|
37
41
|
if (options.adapter) {
|
|
38
42
|
return options.adapter;
|
|
@@ -52,6 +56,110 @@ 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
|
+
}
|
|
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
|
+
}
|
|
55
163
|
export async function doctorProject(options = {}) {
|
|
56
164
|
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
57
165
|
const issues = [];
|
|
@@ -145,9 +253,9 @@ export async function doctorProject(options = {}) {
|
|
|
145
253
|
}
|
|
146
254
|
if (loadedConfig.controlPlane.integrity === 'hash-pinned') {
|
|
147
255
|
notes.push('Integrity: hash-pinned (verify with belay upgrade after runtime changes).');
|
|
148
|
-
const integrity = await verifyIntegrityManifest(repoRoot, activeLayout);
|
|
256
|
+
const integrity = await verifyIntegrityManifest(repoRoot, activeLayout, runtimeIntegrityFiles(activeLayout, scopedPaths));
|
|
149
257
|
if (!integrity.ok) {
|
|
150
|
-
issues.push(`Integrity verification failed: ${integrity.mismatches.
|
|
258
|
+
issues.push(`Integrity verification failed: ${integrity.mismatches.join(', ')}`);
|
|
151
259
|
}
|
|
152
260
|
}
|
|
153
261
|
}
|
|
@@ -163,14 +271,16 @@ export async function doctorProject(options = {}) {
|
|
|
163
271
|
const repoLocalDir = loadedConfig
|
|
164
272
|
? repoLocalStateDirFor(repoRoot, loadedConfig)
|
|
165
273
|
: activeLayout.repoLocalStateDir(repoRoot);
|
|
166
|
-
const
|
|
167
|
-
path.join(hooksDir,
|
|
168
|
-
path.join(hooksDir, 'belay-runner.cmd'),
|
|
274
|
+
const routingOwnerPaths = [
|
|
275
|
+
path.join(hooksDir, runnerFileName(adapterName, process.platform)),
|
|
169
276
|
path.join(hooksDir, 'belay-before-submit.mjs'),
|
|
170
277
|
path.join(hooksDir, 'belay-shell-gate.mjs'),
|
|
171
278
|
path.join(hooksDir, 'belay-tool-gate.mjs'),
|
|
172
279
|
path.join(hooksDir, 'belay-audit.mjs'),
|
|
280
|
+
...(adapterName === 'cursor' ? [path.join(scopedPaths.runtimeDir, 'dispatcher.mjs')] : []),
|
|
173
281
|
corePath,
|
|
282
|
+
];
|
|
283
|
+
const operationalPaths = [
|
|
174
284
|
loadedConfig
|
|
175
285
|
? pendingApprovalsPath(repoRoot, loadedConfig)
|
|
176
286
|
: path.join(repoLocalDir, 'pending-approvals.json'),
|
|
@@ -179,9 +289,67 @@ export async function doctorProject(options = {}) {
|
|
|
179
289
|
: path.join(repoLocalDir, 'approved-approvals.json'),
|
|
180
290
|
path.join(repoRoot, loadedConfig?.audit.logPath ?? activeLayout.defaultAuditLogPath(repoRoot)),
|
|
181
291
|
];
|
|
292
|
+
const requiredPaths = [...routingOwnerPaths, ...operationalPaths];
|
|
182
293
|
for (const requiredPath of requiredPaths) {
|
|
183
294
|
if (!existsSync(requiredPath)) {
|
|
184
295
|
issues.push(`Missing generated file: ${requiredPath}`);
|
|
296
|
+
if (adapterName === 'cursor' && routingOwnerPaths.includes(requiredPath)) {
|
|
297
|
+
issues.push(`Cursor intended ${installScope} owner is incomplete: missing ${requiredPath}. Run belay upgrade --scope ${installScope}.`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (adapterName === 'cursor') {
|
|
302
|
+
issues.push(...(await cursorOriginIssues(hooksDir, installScope, repoRoot)));
|
|
303
|
+
const dispatcherPath = path.join(scopedPaths.runtimeDir, 'dispatcher.mjs');
|
|
304
|
+
if (existsSync(dispatcherPath) &&
|
|
305
|
+
!hasCurrentCursorDispatcherGeneration(await readFile(dispatcherPath, 'utf8'))) {
|
|
306
|
+
issues.push(`Cursor router generation mismatch for intended ${installScope} owner dispatcher: ${dispatcherPath}. Run belay upgrade --scope ${installScope}.`);
|
|
307
|
+
}
|
|
308
|
+
if (installScope === 'project') {
|
|
309
|
+
const globalPaths = resolveScopedPaths(activeLayout, 'global', repoRoot);
|
|
310
|
+
if (existsSync(globalPaths.hooksSettingsPath)) {
|
|
311
|
+
try {
|
|
312
|
+
const { loadHooksFile } = await import('../installer.js');
|
|
313
|
+
const globalHooks = await loadHooksFile(globalPaths.hooksSettingsPath);
|
|
314
|
+
if (hasManagedCursorHookEntries(globalHooks, process.platform, globalPaths.hooksDir, repoRoot)) {
|
|
315
|
+
const globalOwnerFiles = [
|
|
316
|
+
path.join(globalPaths.hooksDir, runnerFileName('cursor', process.platform)),
|
|
317
|
+
path.join(globalPaths.runtimeDir, 'core.mjs'),
|
|
318
|
+
path.join(globalPaths.runtimeDir, 'dispatcher.mjs'),
|
|
319
|
+
...CURSOR_HOOK_SHIMS.map((fileName) => path.join(globalPaths.hooksDir, fileName)),
|
|
320
|
+
];
|
|
321
|
+
const globalOriginProblems = await cursorOriginIssues(globalPaths.hooksDir, 'global', repoRoot);
|
|
322
|
+
issues.push(...globalOriginProblems);
|
|
323
|
+
const globalProblems = [
|
|
324
|
+
...globalOwnerFiles.filter((filePath) => !existsSync(filePath)),
|
|
325
|
+
...globalOriginProblems,
|
|
326
|
+
];
|
|
327
|
+
const globalDispatcherPath = path.join(globalPaths.runtimeDir, 'dispatcher.mjs');
|
|
328
|
+
if (existsSync(globalDispatcherPath)) {
|
|
329
|
+
try {
|
|
330
|
+
if (!hasCurrentCursorDispatcherGeneration(await readFile(globalDispatcherPath, 'utf8'))) {
|
|
331
|
+
globalProblems.push(globalDispatcherPath);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
336
|
+
issues.push(`Unable to inspect shadowed global Cursor dispatcher at ${globalDispatcherPath}: ${detail}.`);
|
|
337
|
+
globalProblems.push(globalDispatcherPath);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (globalProblems.length > 0) {
|
|
341
|
+
issues.push('Global Cursor installation cannot yield to the project owner. Run belay upgrade from this project to refresh the global router.');
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
notes.push('Healthy global Cursor install is shadowed by this project owner and will return neutral for this repository.');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
350
|
+
issues.push(`Unable to inspect shadowed global Cursor hooks at ${globalPaths.hooksSettingsPath}: ${detail}.`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
185
353
|
}
|
|
186
354
|
}
|
|
187
355
|
let hooksOk = true;
|
|
@@ -196,15 +364,17 @@ export async function doctorProject(options = {}) {
|
|
|
196
364
|
const hooksFile = await loadHooksFile(hooksPath);
|
|
197
365
|
for (const { event, definition } of managedEntries) {
|
|
198
366
|
const entries = hooksFile.hooks[event] ?? [];
|
|
199
|
-
const
|
|
200
|
-
if (!
|
|
367
|
+
const matchingEntry = entries.find((entry) => entry.command === definition.command && entry.matcher === definition.matcher);
|
|
368
|
+
if (!matchingEntry || matchingEntry.failClosed !== true) {
|
|
201
369
|
hooksOk = false;
|
|
202
370
|
const matcherSuffix = definition.matcher ? ` (matcher: ${definition.matcher})` : '';
|
|
203
|
-
issues.push(
|
|
371
|
+
issues.push(matchingEntry
|
|
372
|
+
? `Managed Cursor hook for ${event} must set failClosed: true: ${definition.command}${matcherSuffix}`
|
|
373
|
+
: `Missing managed hook for ${event}: ${definition.command}${matcherSuffix}`);
|
|
204
374
|
}
|
|
205
375
|
}
|
|
206
|
-
if (
|
|
207
|
-
warnings.push('
|
|
376
|
+
if (hasLegacyCursorDoubleShellGates(hooksFile, process.platform, hooksDir, repoRoot)) {
|
|
377
|
+
warnings.push('Legacy Cursor Shell preToolUse gate detected alongside beforeShellExecution. Run belay upgrade to remove the managed Shell matcher.');
|
|
208
378
|
}
|
|
209
379
|
}
|
|
210
380
|
else if (adapterName === 'codex') {
|
|
@@ -212,7 +382,7 @@ export async function doctorProject(options = {}) {
|
|
|
212
382
|
// are present in the rendered TOML block.
|
|
213
383
|
const toml = await readFile(hooksPath, 'utf8');
|
|
214
384
|
for (const { event, definition } of managedEntries) {
|
|
215
|
-
if (!toml
|
|
385
|
+
if (!codexHooksTomlIncludesCommand(toml, definition.command)) {
|
|
216
386
|
hooksOk = false;
|
|
217
387
|
issues.push(`Missing Codex managed hook for ${event}: ${definition.command}`);
|
|
218
388
|
}
|
|
@@ -346,6 +516,12 @@ export async function doctorProject(options = {}) {
|
|
|
346
516
|
};
|
|
347
517
|
if (dogfood.active) {
|
|
348
518
|
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.`);
|
|
519
|
+
notes.push(`Host denied after Belay allow (cohort): ${auditVisibility.hostDeniedAfterAllowCount}.`);
|
|
520
|
+
warnings.push(...(await detectUndogfoodedLinkedWorktrees({
|
|
521
|
+
repoRoot,
|
|
522
|
+
adapterName,
|
|
523
|
+
layout: activeLayout,
|
|
524
|
+
})));
|
|
349
525
|
if (dogfood.readyForEnforce) {
|
|
350
526
|
notes.push('Dogfood metrics suggest enforce mode is ready (belay dogfood --enforce).');
|
|
351
527
|
}
|
|
@@ -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) {
|
|
@@ -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;
|
package/dist/config-io.js
CHANGED
|
@@ -209,8 +209,17 @@ export async function loadConfigFile(repoRoot, adapter = detectAdapterName(repoR
|
|
|
209
209
|
}
|
|
210
210
|
export async function writeConfigFile(repoRoot, config, adapter = resolveAdapterName(config)) {
|
|
211
211
|
const configPath = configPathFor(repoRoot, adapter);
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
const directory = path.dirname(configPath);
|
|
213
|
+
const temporaryPath = path.join(directory, `.${path.basename(configPath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
214
|
+
const content = `${JSON.stringify(stripForbiddenShellOverrideLists(config), null, 2)}\n`;
|
|
215
|
+
await mkdir(directory, { recursive: true });
|
|
216
|
+
try {
|
|
217
|
+
await writeFile(temporaryPath, content, { encoding: 'utf8', flag: 'wx' });
|
|
218
|
+
await rename(temporaryPath, configPath);
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
await unlink(temporaryPath).catch(() => undefined);
|
|
222
|
+
}
|
|
214
223
|
}
|
|
215
224
|
export async function mergeAndWriteConfig(repoRoot, adapter = 'cursor') {
|
|
216
225
|
const layout = getAdapterLayout(adapter);
|
|
@@ -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,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;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { isEgressToolHead } from '../../verdict/egress-classify.js';
|
|
3
|
+
const ARGV_DELEGATE_INNER_BLOCKLIST = new Set([
|
|
4
|
+
'sudo',
|
|
5
|
+
'env',
|
|
6
|
+
'command',
|
|
7
|
+
'builtin',
|
|
8
|
+
'exec',
|
|
9
|
+
'time',
|
|
10
|
+
'nice',
|
|
11
|
+
'nohup',
|
|
12
|
+
'stdbuf',
|
|
13
|
+
'setsid',
|
|
14
|
+
'(',
|
|
15
|
+
]);
|
|
16
|
+
export function shouldApplyArgvDelegate(head, innerTokens, depth) {
|
|
17
|
+
if (depth > 0 || isEgressToolHead(head) || head === 'bundle') {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (ARGV_DELEGATE_INNER_BLOCKLIST.has(head)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const innerHead = path.basename(innerTokens[0] ?? '');
|
|
24
|
+
if (ARGV_DELEGATE_INNER_BLOCKLIST.has(innerHead)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return innerTokens.length >= 2;
|
|
28
|
+
}
|
|
29
|
+
export function isGrammarUnknownOnly(requirements, head) {
|
|
30
|
+
if (requirements.length !== 2) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const [spawn, indeterminate] = requirements;
|
|
34
|
+
return (spawn.action === 'process.exec' &&
|
|
35
|
+
spawn.resource.kind === 'executable' &&
|
|
36
|
+
spawn.resource.command === head &&
|
|
37
|
+
spawn.resource.operation === 'spawn' &&
|
|
38
|
+
indeterminate.action === 'indeterminate' &&
|
|
39
|
+
indeterminate.evidence.signals.includes('process.grammar_unknown'));
|
|
40
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ShellEffectRequirement } from '../shell-build.js';
|
|
2
|
+
import type { LowerContext } from './context.js';
|
|
3
|
+
export declare function addRedirectEffects(requirements: ShellEffectRequirement[], tokens: string[], env: Readonly<Record<string, string | undefined>>, context: LowerContext, segment: string): void;
|
|
4
|
+
export declare function effectChangingEnvironmentSignals(head: string, env: Readonly<Record<string, string | undefined>>, changedNames: ReadonlySet<string>): string[];
|