@guilz-dev/belay 0.9.1 → 0.9.3
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 +1 -1
- package/dist/adapters/codex/runtime-entry.d.ts +3 -0
- package/dist/adapters/codex/runtime-entry.js +27 -4
- package/dist/adapters/cursor/cwd-resolution.d.ts +10 -0
- package/dist/adapters/cursor/cwd-resolution.js +58 -0
- package/dist/adapters/cursor/hooks.d.ts +5 -3
- package/dist/adapters/cursor/hooks.js +29 -20
- package/dist/adapters/cursor/runtime-entry.d.ts +1 -0
- package/dist/adapters/cursor/runtime-entry.js +107 -6
- package/dist/adapters/shared/gate-runtime.js +26 -3
- package/dist/adapters/shared/repo-root.js +20 -1
- package/dist/bundle/claude-runtime.mjs +1225 -287
- package/dist/bundle/codex-runtime.mjs +1247 -291
- package/dist/bundle/cursor-runtime.mjs +4320 -3154
- package/dist/cli.js +33 -3
- package/dist/commands/doctor.js +38 -9
- package/dist/commands/health-snapshot.d.ts +3 -0
- package/dist/commands/health-snapshot.js +56 -0
- package/dist/commands/report.js +14 -0
- package/dist/commands/status.js +15 -0
- package/dist/commands/where.d.ts +4 -0
- package/dist/commands/where.js +52 -0
- package/dist/core/approval-repo-lookup.d.ts +16 -0
- package/dist/core/approval-repo-lookup.js +48 -0
- package/dist/core/audit-io.d.ts +1 -1
- package/dist/core/audit-io.js +1 -1
- package/dist/core/audit-legacy-archive.d.ts +1 -0
- package/dist/core/audit-legacy-archive.js +5 -0
- package/dist/core/audit-query.d.ts +1 -0
- package/dist/core/audit-query.js +7 -0
- package/dist/core/audit-serialize.d.ts +3 -0
- package/dist/core/audit-serialize.js +39 -3
- package/dist/core/audit-summary.d.ts +9 -0
- package/dist/core/audit-summary.js +58 -1
- package/dist/core/audit-types.d.ts +4 -0
- package/dist/core/effect-ir/shell-lower.js +283 -27
- package/dist/core/replay-scrub.d.ts +1 -0
- package/dist/core/replay-scrub.js +22 -3
- package/dist/core/shell-tokenizer.d.ts +28 -0
- package/dist/core/shell-tokenizer.js +111 -29
- package/dist/core/verdict/docker-compose-run.d.ts +18 -0
- package/dist/core/verdict/docker-compose-run.js +136 -0
- package/dist/core/verdict/launcher-resolve.js +66 -25
- package/dist/core/verdict/makefile-expand.d.ts +4 -0
- package/dist/core/verdict/makefile-expand.js +151 -0
- package/dist/core/verdict/parser.d.ts +4 -0
- package/dist/core/verdict/parser.js +25 -30
- package/dist/core/verdict/recursive-invocation.d.ts +20 -0
- package/dist/core/verdict/recursive-invocation.js +224 -0
- package/dist/corpus/benign-probe-cores.d.ts +1 -1
- package/dist/corpus/benign-probe-cores.js +2 -0
- package/dist/defaults.js +16 -0
- package/dist/installer/scope-config.d.ts +2 -2
- package/dist/installer.d.ts +10 -1
- package/dist/installer.js +43 -2
- package/dist/types.d.ts +34 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -2
- package/skills/belay/SKILL.md +5 -0
- package/skills/belay/belay-report.md +4 -1
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ equivalent lifecycle points:
|
|
|
44
44
|
| Gate shell / tools / file mutations | `belay-tool-gate` | `beforeShellExecution`, `preToolUse` | `PreToolUse` | `PreToolUse` |
|
|
45
45
|
| Gate subagent launches | `belay-tool-gate` | `subagentStart` | (via `PreToolUse`) | `SubagentStart` |
|
|
46
46
|
| One-shot approvals | `belay-before-submit` | `beforeSubmitPrompt` | `UserPromptSubmit` | `UserPromptSubmit` |
|
|
47
|
-
| Audit log | `belay-audit` | `postToolUse`, `stop`, `sessionEnd` | `PostToolUse` | `PostToolUse` |
|
|
47
|
+
| Audit log | `belay-audit` | `postToolUse`, `postToolUseFailure`, `stop`, `sessionEnd` | `PostToolUse` | `PostToolUse` |
|
|
48
48
|
|
|
49
49
|
## Why
|
|
50
50
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export declare function resolveCodexActionCwd(payload: Record<string, unknown>, fallbackCwd?: string, options?: {
|
|
2
|
+
includeToolInputCwd?: boolean;
|
|
3
|
+
}): string;
|
|
1
4
|
export declare function runBeforeSubmitPromptHook(): Promise<void>;
|
|
2
5
|
export declare function runToolGateHook(eventName: string): Promise<void>;
|
|
3
6
|
export declare function runShellGateHook(): Promise<void>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
import process from 'node:process';
|
|
2
3
|
import { codexLayout } from '../layouts/codex.js';
|
|
3
4
|
import { appendObservedAudit, createDefaultGateRuntimeDeps, evaluateGatedAction, gateUnmappedToolVerdict, gateVerdictToCodexPreToolUseResponse, gateVerdictToCodexUserPromptResponse, processApprovalPrompt, resolveGateConfig, } from '../shared/gate-runtime.js';
|
|
@@ -21,6 +22,22 @@ async function readStdinJson() {
|
|
|
21
22
|
function jsonResponse(value) {
|
|
22
23
|
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
23
24
|
}
|
|
25
|
+
function nonEmptyPathString(value) {
|
|
26
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
27
|
+
}
|
|
28
|
+
export function resolveCodexActionCwd(payload, fallbackCwd = process.cwd(), options = {}) {
|
|
29
|
+
const toolInput = payload.tool_input !== null && typeof payload.tool_input === 'object'
|
|
30
|
+
? payload.tool_input
|
|
31
|
+
: undefined;
|
|
32
|
+
const nestedCwd = options.includeToolInputCwd
|
|
33
|
+
? (nonEmptyPathString(toolInput?.working_directory) ?? nonEmptyPathString(toolInput?.cwd))
|
|
34
|
+
: undefined;
|
|
35
|
+
const payloadCwd = nonEmptyPathString(payload.cwd);
|
|
36
|
+
const workspaceRoot = Array.isArray(payload.workspace_roots)
|
|
37
|
+
? payload.workspace_roots.map(nonEmptyPathString).find(Boolean)
|
|
38
|
+
: undefined;
|
|
39
|
+
return path.resolve(nestedCwd ?? payloadCwd ?? workspaceRoot ?? fallbackCwd);
|
|
40
|
+
}
|
|
24
41
|
async function loadRuntimeContext(cwd) {
|
|
25
42
|
const repoRoot = findRepoRoot(cwd, codexLayout);
|
|
26
43
|
const configPath = codexLayout.configPath(repoRoot);
|
|
@@ -137,7 +154,8 @@ export async function runBeforeSubmitPromptHook() {
|
|
|
137
154
|
try {
|
|
138
155
|
const payload = await readStdinJson();
|
|
139
156
|
const prompt = String(payload.prompt ?? payload.user_message ?? '');
|
|
140
|
-
const
|
|
157
|
+
const cwd = resolveCodexActionCwd(payload);
|
|
158
|
+
const ctx = await loadRuntimeContext(cwd);
|
|
141
159
|
const deps = createDefaultGateRuntimeDeps();
|
|
142
160
|
const result = await processApprovalPrompt(ctx, deps, prompt);
|
|
143
161
|
jsonResponse(gateVerdictToCodexUserPromptResponse(result));
|
|
@@ -155,9 +173,10 @@ export async function runBeforeSubmitPromptHook() {
|
|
|
155
173
|
export async function runToolGateHook(eventName) {
|
|
156
174
|
try {
|
|
157
175
|
const payload = await readStdinJson();
|
|
158
|
-
const cwd = process.cwd();
|
|
159
176
|
const toolName = String(payload.tool_name ?? payload.toolName ?? '');
|
|
160
177
|
const kind = resolveCodexGateKind(eventName, toolName);
|
|
178
|
+
const includeToolInputCwd = (eventName === 'PreToolUse' || eventName === 'preToolUse') && kind === 'shell';
|
|
179
|
+
const cwd = resolveCodexActionCwd(payload, process.cwd(), { includeToolInputCwd });
|
|
161
180
|
const ctx = await loadRuntimeContext(cwd);
|
|
162
181
|
const deps = createDefaultGateRuntimeDeps();
|
|
163
182
|
if (!kind) {
|
|
@@ -201,7 +220,7 @@ export async function runShellGateHook() {
|
|
|
201
220
|
try {
|
|
202
221
|
const payload = await readStdinJson();
|
|
203
222
|
const command = extractString(payload.tool_input, 'command') || String(payload.command ?? '');
|
|
204
|
-
const cwd = process.cwd();
|
|
223
|
+
const cwd = resolveCodexActionCwd(payload, process.cwd(), { includeToolInputCwd: true });
|
|
205
224
|
const ctx = await loadRuntimeContext(cwd);
|
|
206
225
|
const deps = createDefaultGateRuntimeDeps();
|
|
207
226
|
const verdict = await evaluateGatedAction(ctx, deps, {
|
|
@@ -227,7 +246,11 @@ export async function runShellGateHook() {
|
|
|
227
246
|
export async function runAuditHook(eventName) {
|
|
228
247
|
try {
|
|
229
248
|
const payload = await readStdinJson();
|
|
230
|
-
const
|
|
249
|
+
const toolName = String(payload.tool_name ?? payload.toolName ?? '');
|
|
250
|
+
const kind = resolveCodexGateKind(eventName, toolName);
|
|
251
|
+
const includeToolInputCwd = (eventName === 'PreToolUse' || eventName === 'preToolUse') && kind === 'shell';
|
|
252
|
+
const cwd = resolveCodexActionCwd(payload, process.cwd(), { includeToolInputCwd });
|
|
253
|
+
const ctx = await loadRuntimeContext(cwd);
|
|
231
254
|
const deps = createDefaultGateRuntimeDeps();
|
|
232
255
|
await appendObservedAudit(ctx, deps, eventName, payload);
|
|
233
256
|
jsonResponse({});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type CursorActionCwdSource = 'tool_input.working_directory' | 'cwd' | 'workspace_roots' | 'fallback';
|
|
2
|
+
export interface CursorActionCwdResolution {
|
|
3
|
+
cwd: string;
|
|
4
|
+
source: CursorActionCwdSource;
|
|
5
|
+
fromPayload: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function resolveCursorActionCwdDetails(payload: Record<string, unknown>, fallback?: string): CursorActionCwdResolution;
|
|
8
|
+
export declare function isGlobalCursorHookRuntime(): boolean;
|
|
9
|
+
export declare function isUnsafeGlobalHookFallback(resolution: CursorActionCwdResolution, globalRuntime?: boolean): boolean;
|
|
10
|
+
export declare const GLOBAL_HOOK_WORKSPACE_MISSING_MESSAGE: string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
function nonEmptyPathString(value) {
|
|
5
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
6
|
+
}
|
|
7
|
+
export function resolveCursorActionCwdDetails(payload, fallback = process.cwd()) {
|
|
8
|
+
const toolInput = payload.tool_input;
|
|
9
|
+
const toolInputWorkingDirectory = toolInput !== null && typeof toolInput === 'object' && !Array.isArray(toolInput)
|
|
10
|
+
? nonEmptyPathString(toolInput.working_directory)
|
|
11
|
+
: undefined;
|
|
12
|
+
const topLevelCwd = nonEmptyPathString(payload.cwd);
|
|
13
|
+
const workspaceRoot = Array.isArray(payload.workspace_roots)
|
|
14
|
+
? payload.workspace_roots.map(nonEmptyPathString).find(Boolean)
|
|
15
|
+
: undefined;
|
|
16
|
+
if (toolInputWorkingDirectory) {
|
|
17
|
+
return {
|
|
18
|
+
cwd: path.resolve(toolInputWorkingDirectory),
|
|
19
|
+
source: 'tool_input.working_directory',
|
|
20
|
+
fromPayload: true,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (topLevelCwd) {
|
|
24
|
+
return {
|
|
25
|
+
cwd: path.resolve(topLevelCwd),
|
|
26
|
+
source: 'cwd',
|
|
27
|
+
fromPayload: true,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (workspaceRoot) {
|
|
31
|
+
return {
|
|
32
|
+
cwd: path.resolve(workspaceRoot),
|
|
33
|
+
source: 'workspace_roots',
|
|
34
|
+
fromPayload: true,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
cwd: path.resolve(fallback),
|
|
39
|
+
source: 'fallback',
|
|
40
|
+
fromPayload: false,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function isGlobalCursorHookRuntime() {
|
|
44
|
+
try {
|
|
45
|
+
const runtimePath = path.resolve(fileURLToPath(import.meta.url));
|
|
46
|
+
const globalRuntimeDir = path.resolve(path.join(os.homedir(), '.cursor', 'belay', 'runtime'));
|
|
47
|
+
return (runtimePath === globalRuntimeDir || runtimePath.startsWith(`${globalRuntimeDir}${path.sep}`));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function isUnsafeGlobalHookFallback(resolution, globalRuntime = isGlobalCursorHookRuntime()) {
|
|
54
|
+
return !resolution.fromPayload && globalRuntime;
|
|
55
|
+
}
|
|
56
|
+
export const GLOBAL_HOOK_WORKSPACE_MISSING_MESSAGE = 'belay could not determine the workspace for this action from the Cursor hook payload. ' +
|
|
57
|
+
'Open the target project workspace and retry, or run belay doctor. ' +
|
|
58
|
+
'To stop global hooks: belay uninstall --scope global';
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { HookEntry, HooksFile } from '../../types.js';
|
|
2
|
-
/**
|
|
3
|
-
export declare function
|
|
4
|
-
|
|
2
|
+
/** Managed Shell preToolUse gate for Cursor Agent Shell tool invocations. */
|
|
3
|
+
export declare function managedShellPreToolUseEntry(platform: NodeJS.Platform, hooksDir: string, repoRoot: string): HookEntry;
|
|
4
|
+
/** @deprecated Use {@link managedShellPreToolUseEntry}. */
|
|
5
|
+
export declare const legacyManagedShellPreToolUseEntry: typeof managedShellPreToolUseEntry;
|
|
6
|
+
export declare function stripCursorHooksFile(current: HooksFile, platform: NodeJS.Platform, hooksDir: string, repoRoot: string): HooksFile;
|
|
5
7
|
export declare function mergeCursorHooksFile(current: HooksFile, platform: NodeJS.Platform, hooksDir: string, repoRoot: string): HooksFile;
|
|
6
8
|
export declare function hasDuplicateCursorShellGates(hooks: HooksFile, platform: NodeJS.Platform, hooksDir: string, repoRoot: string): boolean;
|
|
@@ -10,37 +10,45 @@ function mergeHookEntry(current, expected, placement) {
|
|
|
10
10
|
}
|
|
11
11
|
return [...filtered, expected];
|
|
12
12
|
}
|
|
13
|
-
/**
|
|
14
|
-
export function
|
|
13
|
+
/** Managed Shell preToolUse gate for Cursor Agent Shell tool invocations. */
|
|
14
|
+
export function managedShellPreToolUseEntry(platform, hooksDir, repoRoot) {
|
|
15
15
|
const managed = getManagedHookEntries(platform, hooksDir, repoRoot);
|
|
16
16
|
const referencePreToolUse = managed.find((entry) => entry.event === 'preToolUse' && entry.definition.matcher === 'Write')?.definition;
|
|
17
17
|
if (!referencePreToolUse) {
|
|
18
|
-
throw new Error('managed hook definitions missing for
|
|
18
|
+
throw new Error('managed hook definitions missing for Shell preToolUse gate');
|
|
19
19
|
}
|
|
20
20
|
return {
|
|
21
21
|
command: referencePreToolUse.command,
|
|
22
22
|
matcher: 'Shell',
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
...hooks,
|
|
33
|
-
hooks: {
|
|
34
|
-
...hooks.hooks,
|
|
35
|
-
preToolUse: preToolUse.filter((entry) => !entryMatches(entry, legacy)),
|
|
36
|
-
},
|
|
25
|
+
/** @deprecated Use {@link managedShellPreToolUseEntry}. */
|
|
26
|
+
export const legacyManagedShellPreToolUseEntry = managedShellPreToolUseEntry;
|
|
27
|
+
export function stripCursorHooksFile(current, platform, hooksDir, repoRoot) {
|
|
28
|
+
const next = {
|
|
29
|
+
version: current.version || 1,
|
|
30
|
+
hooks: { ...current.hooks },
|
|
37
31
|
};
|
|
32
|
+
const managedEntries = getManagedHookEntries(platform, hooksDir, repoRoot);
|
|
33
|
+
for (const { event, definition } of managedEntries) {
|
|
34
|
+
const entries = next.hooks[event];
|
|
35
|
+
if (!Array.isArray(entries)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
next.hooks[event] = entries.filter((entry) => !entryMatches(entry, {
|
|
39
|
+
command: definition.command,
|
|
40
|
+
matcher: definition.matcher,
|
|
41
|
+
}));
|
|
42
|
+
if (next.hooks[event].length === 0) {
|
|
43
|
+
delete next.hooks[event];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return next;
|
|
38
47
|
}
|
|
39
48
|
export function mergeCursorHooksFile(current, platform, hooksDir, repoRoot) {
|
|
40
|
-
const withoutLegacyShell = removeLegacyManagedShellPreToolUse(current, platform, hooksDir, repoRoot);
|
|
41
49
|
const next = {
|
|
42
|
-
version:
|
|
43
|
-
hooks: { ...
|
|
50
|
+
version: current.version || 1,
|
|
51
|
+
hooks: { ...current.hooks },
|
|
44
52
|
};
|
|
45
53
|
const managedEntries = getManagedHookEntries(platform, hooksDir, repoRoot);
|
|
46
54
|
for (const { event, definition } of managedEntries) {
|
|
@@ -52,10 +60,11 @@ export function mergeCursorHooksFile(current, platform, hooksDir, repoRoot) {
|
|
|
52
60
|
return next;
|
|
53
61
|
}
|
|
54
62
|
export function hasDuplicateCursorShellGates(hooks, platform, hooksDir, repoRoot) {
|
|
55
|
-
const
|
|
63
|
+
const shellEntry = managedShellPreToolUseEntry(platform, hooksDir, repoRoot);
|
|
56
64
|
const preToolUse = hooks.hooks.preToolUse;
|
|
57
65
|
if (!Array.isArray(preToolUse)) {
|
|
58
66
|
return false;
|
|
59
67
|
}
|
|
60
|
-
|
|
68
|
+
const shellMatches = preToolUse.filter((entry) => entryMatches(entry, shellEntry));
|
|
69
|
+
return shellMatches.length > 1;
|
|
61
70
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare function resolveCursorActionCwd(payload: Record<string, unknown>, fallback?: string): string;
|
|
1
2
|
export declare function runBeforeSubmitPromptHook(): Promise<void>;
|
|
2
3
|
export declare function runShellGateHook(): Promise<void>;
|
|
3
4
|
export declare function runToolGateHook(eventName: string): Promise<void>;
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
import process from 'node:process';
|
|
3
|
+
import { approvalCommandMatch } from '../../core/approval.js';
|
|
4
|
+
import { findApprovalRepoRoots, formatAmbiguousApprovalRepoMessage, } from '../../core/approval-repo-lookup.js';
|
|
5
|
+
import { DEFAULT_CONFIG_V4 } from '../../core/config.js';
|
|
2
6
|
import { cursorLayout } from '../layouts/cursor.js';
|
|
3
7
|
import { appendObservedAudit, createDefaultGateRuntimeDeps, evaluateGatedAction, gateVerdictToCursorResponse, processApprovalPrompt, resolveGateConfig, } from '../shared/gate-runtime.js';
|
|
4
8
|
import { findRepoRoot } from '../shared/repo-root.js';
|
|
9
|
+
import { GLOBAL_HOOK_WORKSPACE_MISSING_MESSAGE, isUnsafeGlobalHookFallback, resolveCursorActionCwdDetails, } from './cwd-resolution.js';
|
|
5
10
|
async function readStdinJson() {
|
|
6
11
|
const chunks = [];
|
|
7
12
|
for await (const chunk of process.stdin) {
|
|
@@ -21,6 +26,38 @@ async function readStdinJson() {
|
|
|
21
26
|
function jsonResponse(value) {
|
|
22
27
|
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
23
28
|
}
|
|
29
|
+
export function resolveCursorActionCwd(payload, fallback = process.cwd()) {
|
|
30
|
+
return resolveCursorActionCwdDetails(payload, fallback).cwd;
|
|
31
|
+
}
|
|
32
|
+
function resolveCursorToolActionCwdDetails(payload, fallback, eventName, toolName) {
|
|
33
|
+
if ((eventName === 'preToolUse' || eventName === 'PreToolUse') && toolName === 'Shell') {
|
|
34
|
+
return resolveCursorActionCwdDetails(payload, fallback);
|
|
35
|
+
}
|
|
36
|
+
const toolInput = payload.tool_input;
|
|
37
|
+
if (toolInput === null || typeof toolInput !== 'object' || Array.isArray(toolInput)) {
|
|
38
|
+
return resolveCursorActionCwdDetails(payload, fallback);
|
|
39
|
+
}
|
|
40
|
+
const { working_directory: _workingDirectory, ...withoutWorkingDirectory } = toolInput;
|
|
41
|
+
return resolveCursorActionCwdDetails({ ...payload, tool_input: withoutWorkingDirectory }, fallback);
|
|
42
|
+
}
|
|
43
|
+
function collectCandidateRepoRoots(payload, resolution) {
|
|
44
|
+
const roots = [];
|
|
45
|
+
const add = (value) => {
|
|
46
|
+
const resolved = path.resolve(value);
|
|
47
|
+
if (!roots.includes(resolved)) {
|
|
48
|
+
roots.push(resolved);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
if (Array.isArray(payload.workspace_roots)) {
|
|
52
|
+
for (const root of payload.workspace_roots) {
|
|
53
|
+
if (typeof root === 'string' && root.trim()) {
|
|
54
|
+
add(root);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
add(resolution.cwd);
|
|
59
|
+
return roots;
|
|
60
|
+
}
|
|
24
61
|
async function loadRuntimeContext(cwd) {
|
|
25
62
|
const repoRoot = findRepoRoot(cwd, cursorLayout);
|
|
26
63
|
const configPath = cursorLayout.configPath(repoRoot);
|
|
@@ -28,6 +65,39 @@ async function loadRuntimeContext(cwd) {
|
|
|
28
65
|
const config = await resolveGateConfig({ layout: cursorLayout, repoRoot, configPath }, deps);
|
|
29
66
|
return { layout: cursorLayout, repoRoot, config, configPath };
|
|
30
67
|
}
|
|
68
|
+
async function processApprovalPromptWithRepoFallback(ctx, deps, prompt, payload, resolution) {
|
|
69
|
+
const tokenPrefix = ctx.config.tokenPrefix || DEFAULT_CONFIG_V4.tokenPrefix;
|
|
70
|
+
const approvalId = approvalCommandMatch(prompt, tokenPrefix);
|
|
71
|
+
const result = await processApprovalPrompt(ctx, deps, prompt);
|
|
72
|
+
if (result.continue || !approvalId) {
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
const notFound = result.user_message?.includes('not found') ||
|
|
76
|
+
result.user_message?.includes('Belay approval not found');
|
|
77
|
+
if (!notFound) {
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
const candidates = collectCandidateRepoRoots(payload, resolution).filter((root) => root !== ctx.repoRoot);
|
|
81
|
+
if (candidates.length === 0) {
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
const lookup = await findApprovalRepoRoots({
|
|
85
|
+
approvalId,
|
|
86
|
+
candidateRepoRoots: candidates,
|
|
87
|
+
adapter: 'cursor',
|
|
88
|
+
});
|
|
89
|
+
if (lookup.status === 'ambiguous') {
|
|
90
|
+
return {
|
|
91
|
+
continue: false,
|
|
92
|
+
user_message: formatAmbiguousApprovalRepoMessage(lookup.repoRoots),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (lookup.status !== 'found') {
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
const retryCtx = await loadRuntimeContext(lookup.repoRoot);
|
|
99
|
+
return processApprovalPrompt(retryCtx, deps, prompt);
|
|
100
|
+
}
|
|
31
101
|
function isSubagentEvent(payload, eventName) {
|
|
32
102
|
return eventName === 'subagentStart' || payload.subagent_type !== undefined;
|
|
33
103
|
}
|
|
@@ -38,9 +108,17 @@ export async function runBeforeSubmitPromptHook() {
|
|
|
38
108
|
try {
|
|
39
109
|
const payload = await readStdinJson();
|
|
40
110
|
const prompt = String(payload.prompt ?? '');
|
|
41
|
-
const
|
|
111
|
+
const resolution = resolveCursorActionCwdDetails(payload);
|
|
112
|
+
if (isUnsafeGlobalHookFallback(resolution)) {
|
|
113
|
+
jsonResponse({
|
|
114
|
+
continue: false,
|
|
115
|
+
user_message: GLOBAL_HOOK_WORKSPACE_MISSING_MESSAGE,
|
|
116
|
+
});
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const ctx = await loadRuntimeContext(resolution.cwd);
|
|
42
120
|
const deps = createDefaultGateRuntimeDeps();
|
|
43
|
-
const result = await
|
|
121
|
+
const result = await processApprovalPromptWithRepoFallback(ctx, deps, prompt, payload, resolution);
|
|
44
122
|
jsonResponse({
|
|
45
123
|
continue: result.continue,
|
|
46
124
|
...(result.user_message ? { user_message: result.user_message } : {}),
|
|
@@ -58,13 +136,22 @@ export async function runShellGateHook() {
|
|
|
58
136
|
try {
|
|
59
137
|
const payload = await readStdinJson();
|
|
60
138
|
const command = String(payload.command ?? '').trim();
|
|
61
|
-
const
|
|
139
|
+
const resolution = resolveCursorActionCwdDetails(payload);
|
|
140
|
+
if (isUnsafeGlobalHookFallback(resolution)) {
|
|
141
|
+
jsonResponse({
|
|
142
|
+
permission: 'deny',
|
|
143
|
+
user_message: GLOBAL_HOOK_WORKSPACE_MISSING_MESSAGE,
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const cwd = resolution.cwd;
|
|
62
148
|
const ctx = await loadRuntimeContext(cwd);
|
|
63
149
|
const deps = createDefaultGateRuntimeDeps();
|
|
64
150
|
const verdict = await evaluateGatedAction(ctx, deps, {
|
|
65
151
|
kind: 'shell',
|
|
66
152
|
cwd,
|
|
67
153
|
command,
|
|
154
|
+
payload,
|
|
68
155
|
sourceEvent: 'beforeShellExecution',
|
|
69
156
|
});
|
|
70
157
|
jsonResponse(gateVerdictToCursorResponse(verdict));
|
|
@@ -79,10 +166,18 @@ export async function runShellGateHook() {
|
|
|
79
166
|
export async function runToolGateHook(eventName) {
|
|
80
167
|
try {
|
|
81
168
|
const payload = await readStdinJson();
|
|
82
|
-
const
|
|
169
|
+
const toolName = String(payload.tool_name ?? '');
|
|
170
|
+
const resolution = resolveCursorToolActionCwdDetails(payload, process.cwd(), eventName, toolName);
|
|
171
|
+
if (isUnsafeGlobalHookFallback(resolution)) {
|
|
172
|
+
jsonResponse({
|
|
173
|
+
permission: 'deny',
|
|
174
|
+
user_message: GLOBAL_HOOK_WORKSPACE_MISSING_MESSAGE,
|
|
175
|
+
});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const cwd = resolution.cwd;
|
|
83
179
|
const ctx = await loadRuntimeContext(cwd);
|
|
84
180
|
const deps = createDefaultGateRuntimeDeps();
|
|
85
|
-
const toolName = String(payload.tool_name ?? '');
|
|
86
181
|
if (isSubagentEvent(payload, eventName)) {
|
|
87
182
|
const verdict = await evaluateGatedAction(ctx, deps, {
|
|
88
183
|
kind: 'subagent',
|
|
@@ -137,7 +232,13 @@ export async function runToolGateHook(eventName) {
|
|
|
137
232
|
export async function runAuditHook(eventName) {
|
|
138
233
|
try {
|
|
139
234
|
const payload = await readStdinJson();
|
|
140
|
-
const
|
|
235
|
+
const toolName = String(payload.tool_name ?? '');
|
|
236
|
+
const resolution = resolveCursorToolActionCwdDetails(payload, process.cwd(), eventName, toolName);
|
|
237
|
+
if (isUnsafeGlobalHookFallback(resolution)) {
|
|
238
|
+
jsonResponse({});
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const ctx = await loadRuntimeContext(resolution.cwd);
|
|
141
242
|
const deps = createDefaultGateRuntimeDeps();
|
|
142
243
|
await appendObservedAudit(ctx, deps, eventName, payload);
|
|
143
244
|
jsonResponse({});
|
|
@@ -7,7 +7,7 @@ import { buildReplayHint, buildRetryInstructionForConfig, canAutoReplay, getExec
|
|
|
7
7
|
import { claimApprovedForReplay, gateApprovalStoreFromDeps, recordApproval, } from '../../core/approval-service.js';
|
|
8
8
|
import { issueApprovalToken } from '../../core/approval-token.js';
|
|
9
9
|
import { buildAuditActionSnapshot, buildAuditReplayContext, } from '../../core/audit-replay-context.js';
|
|
10
|
-
import { approvalCorrelationId, serializeAuditRecordV3 } from '../../core/audit-serialize.js';
|
|
10
|
+
import { approvalCorrelationId, serializeAuditRecordV3, toolInvocationCorrelationId, } from '../../core/audit-serialize.js';
|
|
11
11
|
import { boundedUtf8Tail } from '../../core/bounded-output.js';
|
|
12
12
|
import { mutateApprovalStateWithRetry } from '../../core/capability/approval-state-mutation.js';
|
|
13
13
|
import { APPROVAL_STATE_VERSION_V3 } from '../../core/capability/approval-v3.js';
|
|
@@ -36,7 +36,7 @@ import { extractJudgeFallbackReason, formatJudgeInfrastructureDenyMessage, infer
|
|
|
36
36
|
import { notifyDeny } from '../../core/notify.js';
|
|
37
37
|
import { canonicalPath } from '../../core/path-utils.js';
|
|
38
38
|
import { recoveryFailClosedResult, recoveryFailReasonFromSkip, } from '../../core/recovery/fail-closed.js';
|
|
39
|
-
import { fingerprintReplayPayload } from '../../core/replay-scrub.js';
|
|
39
|
+
import { fingerprintReplayPayload, redactToolInvocationId } from '../../core/replay-scrub.js';
|
|
40
40
|
import { FILE_CHECKPOINT_ISOLATION_UNAVAILABLE, isTransactionalEligible, runTransactionalExecution, TRANSACTIONAL_ALREADY_APPLIED, TRANSACTIONAL_APPROVAL_BYPASS_REASONS, } from '../../core/transactional/index.js';
|
|
41
41
|
import { buildAuditProvenanceFields } from '../../runtime-provenance.js';
|
|
42
42
|
import { egressStatus } from '../../services/egress-service.js';
|
|
@@ -616,6 +616,9 @@ export async function evaluateGatedAction(ctx, deps, params) {
|
|
|
616
616
|
event: resolveGateAuditEvent(sourceEvent, params.kind),
|
|
617
617
|
sourceEvent,
|
|
618
618
|
kind: params.kind,
|
|
619
|
+
...(typeof params.payload?.tool_use_id === 'string'
|
|
620
|
+
? { toolInvocationCorrelationId: toolInvocationCorrelationId(params.payload.tool_use_id) }
|
|
621
|
+
: {}),
|
|
619
622
|
fingerprint: verdict.fingerprint,
|
|
620
623
|
verdict: verdict.verdict,
|
|
621
624
|
reason: verdict.reason,
|
|
@@ -779,6 +782,9 @@ export async function evaluateGatedAction(ctx, deps, params) {
|
|
|
779
782
|
const scrubbedPayload = fingerprintReplayPayload(params.kind, params.payload, scrubOpts);
|
|
780
783
|
return gateDecisionToVerdict(ctx, deps, params.kind, result, {
|
|
781
784
|
sourceEvent: params.sourceEvent,
|
|
785
|
+
toolInvocationCorrelationId: typeof params.payload?.tool_use_id === 'string'
|
|
786
|
+
? toolInvocationCorrelationId(params.payload.tool_use_id)
|
|
787
|
+
: undefined,
|
|
782
788
|
predictedAssessment,
|
|
783
789
|
observedAssessment,
|
|
784
790
|
transactionalLayer,
|
|
@@ -904,6 +910,9 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
904
910
|
event: auditEvent,
|
|
905
911
|
sourceEvent,
|
|
906
912
|
kind,
|
|
913
|
+
...(auditExtras.toolInvocationCorrelationId
|
|
914
|
+
? { toolInvocationCorrelationId: auditExtras.toolInvocationCorrelationId }
|
|
915
|
+
: {}),
|
|
907
916
|
fingerprint: result.fingerprint,
|
|
908
917
|
summary: result.normalizedCommand ?? result.summary ?? '',
|
|
909
918
|
assessment: result.assessment,
|
|
@@ -1398,12 +1407,26 @@ export function gateVerdictToCodexUserPromptResponse(verdict) {
|
|
|
1398
1407
|
};
|
|
1399
1408
|
}
|
|
1400
1409
|
export async function appendObservedAudit(ctx, deps, eventName, payload) {
|
|
1410
|
+
const rawToolUseId = typeof payload.tool_use_id === 'string' ? payload.tool_use_id : undefined;
|
|
1411
|
+
const summaryPayload = redactToolInvocationId(payload, rawToolUseId);
|
|
1401
1412
|
await deps.appendAudit(ctx, {
|
|
1402
1413
|
event: eventName,
|
|
1403
1414
|
kind: 'audit',
|
|
1404
1415
|
verdict: 'allow',
|
|
1405
1416
|
reason: 'observed',
|
|
1406
|
-
|
|
1417
|
+
...(rawToolUseId
|
|
1418
|
+
? { toolInvocationCorrelationId: toolInvocationCorrelationId(rawToolUseId) }
|
|
1419
|
+
: {}),
|
|
1420
|
+
...(typeof summaryPayload.tool_name === 'string' ? { toolName: summaryPayload.tool_name } : {}),
|
|
1421
|
+
...(typeof summaryPayload.failure_type === 'string'
|
|
1422
|
+
? { failureType: summaryPayload.failure_type }
|
|
1423
|
+
: {}),
|
|
1424
|
+
...(typeof summaryPayload.error_message === 'string'
|
|
1425
|
+
? { errorMessage: summaryPayload.error_message }
|
|
1426
|
+
: {}),
|
|
1427
|
+
...(typeof payload.duration === 'number' ? { durationMs: payload.duration } : {}),
|
|
1428
|
+
...(typeof payload.is_interrupt === 'boolean' ? { isInterrupt: payload.is_interrupt } : {}),
|
|
1429
|
+
summary: canonicalStringify(summaryPayload),
|
|
1407
1430
|
});
|
|
1408
1431
|
}
|
|
1409
1432
|
export { GateNormalizationError };
|
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
function belayConfigPath(current, adapterName) {
|
|
4
|
+
if (adapterName === 'cursor') {
|
|
5
|
+
return path.join(current, '.cursor', 'belay.config.json');
|
|
6
|
+
}
|
|
7
|
+
if (adapterName === 'claude') {
|
|
8
|
+
return path.join(current, '.claude', 'belay.config.json');
|
|
9
|
+
}
|
|
10
|
+
return path.join(current, '.codex', 'belay.config.json');
|
|
11
|
+
}
|
|
12
|
+
function markerMatches(current, marker, layout) {
|
|
13
|
+
const markerPath = path.join(current, marker);
|
|
14
|
+
if (!existsSync(markerPath)) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (marker === '.cursor' || marker === '.claude' || marker === '.codex') {
|
|
18
|
+
return existsSync(belayConfigPath(current, layout.name));
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
3
22
|
export function findRepoRoot(startPath, layout) {
|
|
4
23
|
let current = path.resolve(startPath);
|
|
5
24
|
while (true) {
|
|
6
25
|
for (const marker of layout.repoRootMarkers) {
|
|
7
|
-
if (
|
|
26
|
+
if (markerMatches(current, marker, layout)) {
|
|
8
27
|
return current;
|
|
9
28
|
}
|
|
10
29
|
}
|