@guilz-dev/belay 0.9.2 → 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.
Files changed (118) hide show
  1. package/README.md +36 -2
  2. package/dist/adapters/codex/hooks.d.ts +1 -0
  3. package/dist/adapters/codex/hooks.js +3 -0
  4. package/dist/adapters/codex/runtime-entry.d.ts +3 -0
  5. package/dist/adapters/codex/runtime-entry.js +27 -4
  6. package/dist/adapters/cursor/cwd-resolution.d.ts +10 -0
  7. package/dist/adapters/cursor/cwd-resolution.js +58 -0
  8. package/dist/adapters/cursor/dispatcher-generation.d.ts +3 -0
  9. package/dist/adapters/cursor/dispatcher-generation.js +5 -0
  10. package/dist/adapters/cursor/hook-dispatch-entry.d.ts +7 -0
  11. package/dist/adapters/cursor/hook-dispatch-entry.js +126 -0
  12. package/dist/adapters/cursor/hook-router.d.ts +23 -0
  13. package/dist/adapters/cursor/hook-router.js +266 -0
  14. package/dist/adapters/cursor/hooks.d.ts +5 -0
  15. package/dist/adapters/cursor/hooks.js +115 -3
  16. package/dist/adapters/cursor/routing-layout.d.ts +5 -0
  17. package/dist/adapters/cursor/routing-layout.js +29 -0
  18. package/dist/adapters/cursor/runtime-entry.d.ts +18 -0
  19. package/dist/adapters/cursor/runtime-entry.js +135 -34
  20. package/dist/adapters/layouts/scope.d.ts +5 -0
  21. package/dist/adapters/layouts/scope.js +71 -0
  22. package/dist/adapters/shared/gate-runtime.js +27 -3
  23. package/dist/adapters/shared/repo-root.js +20 -1
  24. package/dist/bundle/claude-runtime.mjs +2660 -1951
  25. package/dist/bundle/codex-runtime.mjs +2680 -1953
  26. package/dist/bundle/cursor-dispatcher.mjs +443 -0
  27. package/dist/bundle/cursor-runtime.mjs +5638 -4697
  28. package/dist/cli.js +33 -3
  29. package/dist/commands/doctor.js +215 -16
  30. package/dist/commands/health-snapshot.d.ts +3 -0
  31. package/dist/commands/health-snapshot.js +61 -0
  32. package/dist/commands/recovery-checkpoints.d.ts +3 -3
  33. package/dist/commands/report.js +14 -0
  34. package/dist/commands/status.js +15 -0
  35. package/dist/commands/where.d.ts +4 -0
  36. package/dist/commands/where.js +52 -0
  37. package/dist/config-io.js +11 -2
  38. package/dist/core/approval-repo-lookup.d.ts +16 -0
  39. package/dist/core/approval-repo-lookup.js +48 -0
  40. package/dist/core/audit-io.d.ts +1 -1
  41. package/dist/core/audit-io.js +1 -1
  42. package/dist/core/audit-legacy-archive.js +43 -25
  43. package/dist/core/audit-query.d.ts +1 -0
  44. package/dist/core/audit-query.js +7 -0
  45. package/dist/core/audit-serialize.d.ts +3 -0
  46. package/dist/core/audit-serialize.js +39 -3
  47. package/dist/core/audit-summary.d.ts +9 -0
  48. package/dist/core/audit-summary.js +58 -1
  49. package/dist/core/audit-types.d.ts +4 -0
  50. package/dist/core/classify-tool.js +16 -8
  51. package/dist/core/effect-ir/argv-delegate.d.ts +9 -0
  52. package/dist/core/effect-ir/argv-delegate.js +42 -0
  53. package/dist/core/effect-ir/shell-lower/argv-delegate-gate.d.ts +3 -0
  54. package/dist/core/effect-ir/shell-lower/argv-delegate-gate.js +40 -0
  55. package/dist/core/effect-ir/shell-lower/augment.d.ts +4 -0
  56. package/dist/core/effect-ir/shell-lower/augment.js +97 -0
  57. package/dist/core/effect-ir/shell-lower/context.d.ts +10 -0
  58. package/dist/core/effect-ir/shell-lower/context.js +1 -0
  59. package/dist/core/effect-ir/shell-lower/decode-process.d.ts +9 -0
  60. package/dist/core/effect-ir/shell-lower/decode-process.js +201 -0
  61. package/dist/core/effect-ir/shell-lower/decoders/belay.d.ts +2 -0
  62. package/dist/core/effect-ir/shell-lower/decoders/belay.js +29 -0
  63. package/dist/core/effect-ir/shell-lower/decoders/builtins.d.ts +7 -0
  64. package/dist/core/effect-ir/shell-lower/decoders/builtins.js +194 -0
  65. package/dist/core/effect-ir/shell-lower/decoders/decode-package-exec.d.ts +4 -0
  66. package/dist/core/effect-ir/shell-lower/decoders/decode-package-exec.js +68 -0
  67. package/dist/core/effect-ir/shell-lower/decoders/docker.d.ts +3 -0
  68. package/dist/core/effect-ir/shell-lower/decoders/docker.js +43 -0
  69. package/dist/core/effect-ir/shell-lower/decoders/filesystem.d.ts +9 -0
  70. package/dist/core/effect-ir/shell-lower/decoders/filesystem.js +217 -0
  71. package/dist/core/effect-ir/shell-lower/decoders/prisma.d.ts +2 -0
  72. package/dist/core/effect-ir/shell-lower/decoders/prisma.js +54 -0
  73. package/dist/core/effect-ir/shell-lower/decoders/ruby.d.ts +11 -0
  74. package/dist/core/effect-ir/shell-lower/decoders/ruby.js +147 -0
  75. package/dist/core/effect-ir/shell-lower/decoders/toolchain.d.ts +7 -0
  76. package/dist/core/effect-ir/shell-lower/decoders/toolchain.js +155 -0
  77. package/dist/core/effect-ir/shell-lower/requirement.d.ts +12 -0
  78. package/dist/core/effect-ir/shell-lower/requirement.js +65 -0
  79. package/dist/core/effect-ir/shell-lower/segment.d.ts +11 -0
  80. package/dist/core/effect-ir/shell-lower/segment.js +82 -0
  81. package/dist/core/effect-ir/shell-lower/tokens.d.ts +26 -0
  82. package/dist/core/effect-ir/shell-lower/tokens.js +183 -0
  83. package/dist/core/effect-ir/shell-lower.d.ts +2 -7
  84. package/dist/core/effect-ir/shell-lower.js +122 -1511
  85. package/dist/core/glob.js +32 -16
  86. package/dist/core/integrity.d.ts +2 -2
  87. package/dist/core/integrity.js +52 -12
  88. package/dist/core/replay-scrub.d.ts +3 -0
  89. package/dist/core/replay-scrub.js +30 -3
  90. package/dist/core/shell-substitution.js +2 -2
  91. package/dist/core/shell-tokenizer.d.ts +28 -0
  92. package/dist/core/shell-tokenizer.js +111 -29
  93. package/dist/core/shell-unparseable.js +3 -1
  94. package/dist/core/verdict/docker-compose-run.d.ts +18 -0
  95. package/dist/core/verdict/docker-compose-run.js +136 -0
  96. package/dist/core/verdict/launcher-resolve.js +28 -28
  97. package/dist/core/verdict/parser.d.ts +3 -0
  98. package/dist/core/verdict/parser.js +23 -40
  99. package/dist/core/verdict/recursive-invocation.d.ts +20 -0
  100. package/dist/core/verdict/recursive-invocation.js +224 -0
  101. package/dist/defaults.d.ts +7 -3
  102. package/dist/defaults.js +35 -21
  103. package/dist/installer/bootstrap.d.ts +1 -0
  104. package/dist/installer/bootstrap.js +2 -2
  105. package/dist/installer/runtime-artifacts.js +43 -13
  106. package/dist/installer/scope-config.d.ts +2 -2
  107. package/dist/installer.d.ts +10 -1
  108. package/dist/installer.js +83 -7
  109. package/dist/node-resolution.d.ts +1 -0
  110. package/dist/node-resolution.js +61 -0
  111. package/dist/templates.d.ts +7 -4
  112. package/dist/templates.js +42 -4
  113. package/dist/types.d.ts +35 -1
  114. package/dist/version.d.ts +1 -1
  115. package/dist/version.js +1 -1
  116. package/package.json +5 -2
  117. package/skills/belay/SKILL.md +5 -0
  118. package/skills/belay/belay-report.md +4 -1
@@ -1,10 +1,34 @@
1
+ import { existsSync, realpathSync } from 'node:fs';
1
2
  import { getManagedHookEntries } from '../../defaults.js';
2
3
  function entryMatches(existing, expected) {
3
4
  return existing.command === expected.command && existing.matcher === expected.matcher;
4
5
  }
5
- function mergeHookEntry(current, expected, placement) {
6
+ function legacyManagedEntries(platform, hooksDir, repoRoot) {
7
+ const entries = [
8
+ ...getManagedHookEntries(platform, hooksDir, repoRoot, 'legacy-relative'),
9
+ ...getManagedHookEntries(platform, hooksDir, repoRoot, 'legacy-absolute'),
10
+ ...getManagedHookEntries(platform, hooksDir, repoRoot, 'legacy-quoted-absolute'),
11
+ ...getManagedHookEntries(platform, hooksDir, repoRoot, 'legacy-bare-powershell-absolute'),
12
+ ];
13
+ if (existsSync(hooksDir)) {
14
+ const canonicalHooksDir = realpathSync(hooksDir);
15
+ if (canonicalHooksDir !== hooksDir) {
16
+ entries.push(...getManagedHookEntries(platform, canonicalHooksDir, repoRoot, 'legacy-absolute'));
17
+ }
18
+ }
19
+ return entries;
20
+ }
21
+ function variantsForDefinition(legacyEntries, event, definition) {
22
+ return [
23
+ definition,
24
+ ...legacyEntries
25
+ .filter((entry) => entry.event === event && entry.definition.matcher === definition.matcher)
26
+ .map((entry) => entry.definition),
27
+ ];
28
+ }
29
+ function mergeHookEntry(current, expected, managedVariants, placement) {
6
30
  const entries = Array.isArray(current) ? [...current] : [];
7
- const filtered = entries.filter((entry) => !entryMatches(entry, expected));
31
+ const filtered = entries.filter((entry) => !managedVariants.some((variant) => entryMatches(entry, variant)));
8
32
  if (placement === 'prepend') {
9
33
  return [expected, ...filtered];
10
34
  }
@@ -20,24 +44,105 @@ export function managedShellPreToolUseEntry(platform, hooksDir, repoRoot) {
20
44
  return {
21
45
  command: referencePreToolUse.command,
22
46
  matcher: 'Shell',
47
+ failClosed: true,
23
48
  };
24
49
  }
25
50
  /** @deprecated Use {@link managedShellPreToolUseEntry}. */
26
51
  export const legacyManagedShellPreToolUseEntry = managedShellPreToolUseEntry;
52
+ export function stripCursorHooksFile(current, platform, hooksDir, repoRoot) {
53
+ const next = {
54
+ version: current.version || 1,
55
+ hooks: { ...current.hooks },
56
+ };
57
+ const managedEntries = getManagedHookEntries(platform, hooksDir, repoRoot);
58
+ const legacyEntries = legacyManagedEntries(platform, hooksDir, repoRoot);
59
+ for (const { event, definition } of managedEntries) {
60
+ const entries = next.hooks[event];
61
+ if (!Array.isArray(entries)) {
62
+ continue;
63
+ }
64
+ const variants = variantsForDefinition(legacyEntries, event, definition);
65
+ next.hooks[event] = entries.filter((entry) => !variants.some((variant) => entryMatches(entry, variant)));
66
+ if (next.hooks[event].length === 0) {
67
+ delete next.hooks[event];
68
+ }
69
+ }
70
+ return stripLegacyManagedShellPreToolUseGates(next, platform, hooksDir, repoRoot);
71
+ }
27
72
  export function mergeCursorHooksFile(current, platform, hooksDir, repoRoot) {
28
73
  const next = {
29
74
  version: current.version || 1,
30
75
  hooks: { ...current.hooks },
31
76
  };
32
77
  const managedEntries = getManagedHookEntries(platform, hooksDir, repoRoot);
78
+ const legacyEntries = legacyManagedEntries(platform, hooksDir, repoRoot);
33
79
  for (const { event, definition } of managedEntries) {
80
+ const variants = variantsForDefinition(legacyEntries, event, definition);
34
81
  next.hooks[event] = mergeHookEntry(next.hooks[event], {
35
82
  command: definition.command,
36
83
  matcher: definition.matcher,
37
- }, definition.placement);
84
+ failClosed: definition.failClosed,
85
+ }, variants, definition.placement);
86
+ }
87
+ return stripLegacyManagedShellPreToolUseGates(next, platform, hooksDir, repoRoot);
88
+ }
89
+ export function legacyManagedShellPreToolUseVariants(platform, hooksDir, repoRoot) {
90
+ const shellEntry = managedShellPreToolUseEntry(platform, hooksDir, repoRoot);
91
+ const legacyEntries = legacyManagedEntries(platform, hooksDir, repoRoot);
92
+ const commands = new Set([shellEntry.command]);
93
+ for (const entry of legacyEntries) {
94
+ if (entry.event === 'preToolUse' && entry.definition.matcher === 'Write') {
95
+ commands.add(entry.definition.command);
96
+ }
97
+ }
98
+ return [...commands].map((command) => ({
99
+ command,
100
+ matcher: 'Shell',
101
+ failClosed: true,
102
+ }));
103
+ }
104
+ function stripLegacyManagedShellPreToolUseGates(hooks, platform, hooksDir, repoRoot) {
105
+ const variants = legacyManagedShellPreToolUseVariants(platform, hooksDir, repoRoot);
106
+ const preToolUse = hooks.hooks.preToolUse;
107
+ if (!Array.isArray(preToolUse) || variants.length === 0) {
108
+ return hooks;
109
+ }
110
+ const filtered = preToolUse.filter((entry) => !variants.some((variant) => entryMatches(entry, variant)));
111
+ if (filtered.length === preToolUse.length) {
112
+ return hooks;
113
+ }
114
+ const next = {
115
+ version: hooks.version || 1,
116
+ hooks: { ...hooks.hooks },
117
+ };
118
+ if (filtered.length === 0) {
119
+ delete next.hooks.preToolUse;
120
+ }
121
+ else {
122
+ next.hooks.preToolUse = filtered;
38
123
  }
39
124
  return next;
40
125
  }
126
+ export function hasLegacyCursorDoubleShellGates(hooks, platform, hooksDir, repoRoot) {
127
+ const shellVariants = legacyManagedShellPreToolUseVariants(platform, hooksDir, repoRoot);
128
+ const preToolUse = hooks.hooks.preToolUse;
129
+ const beforeShell = hooks.hooks.beforeShellExecution;
130
+ if (!Array.isArray(preToolUse) || !Array.isArray(beforeShell)) {
131
+ return false;
132
+ }
133
+ const hasLegacyShellPreToolUse = preToolUse.some((entry) => shellVariants.some((variant) => entryMatches(entry, variant)));
134
+ if (!hasLegacyShellPreToolUse) {
135
+ return false;
136
+ }
137
+ const managedEntries = getManagedHookEntries(platform, hooksDir, repoRoot);
138
+ const shellManaged = managedEntries.find((entry) => entry.event === 'beforeShellExecution');
139
+ if (!shellManaged) {
140
+ return false;
141
+ }
142
+ const shellVariantsForBefore = variantsForDefinition(legacyManagedEntries(platform, hooksDir, repoRoot), 'beforeShellExecution', shellManaged.definition);
143
+ return beforeShell.some((entry) => shellVariantsForBefore.some((variant) => entryMatches(entry, variant)));
144
+ }
145
+ /** @deprecated Use {@link hasLegacyCursorDoubleShellGates}. */
41
146
  export function hasDuplicateCursorShellGates(hooks, platform, hooksDir, repoRoot) {
42
147
  const shellEntry = managedShellPreToolUseEntry(platform, hooksDir, repoRoot);
43
148
  const preToolUse = hooks.hooks.preToolUse;
@@ -47,3 +152,10 @@ export function hasDuplicateCursorShellGates(hooks, platform, hooksDir, repoRoot
47
152
  const shellMatches = preToolUse.filter((entry) => entryMatches(entry, shellEntry));
48
153
  return shellMatches.length > 1;
49
154
  }
155
+ export function hasManagedCursorHookEntries(hooks, platform, hooksDir, repoRoot) {
156
+ const managedEntries = [
157
+ ...getManagedHookEntries(platform, hooksDir, repoRoot),
158
+ ...legacyManagedEntries(platform, hooksDir, repoRoot),
159
+ ];
160
+ return managedEntries.some(({ event, definition }) => hooks.hooks[event]?.some((entry) => entryMatches(entry, definition)));
161
+ }
@@ -0,0 +1,5 @@
1
+ export declare function cursorRoutingConfigPath(repoRoot: string): string;
2
+ export declare function cursorRoutingHooksDir(repoRoot: string): string;
3
+ export declare function cursorRoutingHooksSettingsPath(repoRoot: string): string;
4
+ export declare function cursorRoutingRuntimeDir(repoRoot: string): string;
5
+ export declare function findCursorRoutingRepoRoot(startPath: string): string;
@@ -0,0 +1,29 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ export function cursorRoutingConfigPath(repoRoot) {
4
+ return path.join(repoRoot, '.cursor', 'belay.config.json');
5
+ }
6
+ export function cursorRoutingHooksDir(repoRoot) {
7
+ return path.join(repoRoot, '.cursor', 'hooks');
8
+ }
9
+ export function cursorRoutingHooksSettingsPath(repoRoot) {
10
+ return path.join(repoRoot, '.cursor', 'hooks.json');
11
+ }
12
+ export function cursorRoutingRuntimeDir(repoRoot) {
13
+ return path.join(repoRoot, '.cursor', 'belay', 'runtime');
14
+ }
15
+ export function findCursorRoutingRepoRoot(startPath) {
16
+ const resolvedStart = path.resolve(startPath);
17
+ let current = resolvedStart;
18
+ while (true) {
19
+ if (existsSync(path.join(current, '.git')) ||
20
+ (existsSync(path.join(current, '.cursor')) && existsSync(cursorRoutingConfigPath(current)))) {
21
+ return current;
22
+ }
23
+ const parent = path.dirname(current);
24
+ if (parent === current) {
25
+ return resolvedStart;
26
+ }
27
+ current = parent;
28
+ }
29
+ }
@@ -1,4 +1,22 @@
1
+ export declare function resolveCursorActionCwd(payload: Record<string, unknown>, fallback?: string): string;
2
+ export declare function handleBeforeSubmitPromptHook(payload: Record<string, unknown>): Promise<{
3
+ replay?: import("../../core/approval-replay.js").ApprovalReplayHint | undefined;
4
+ user_message?: string | undefined;
5
+ continue: boolean;
6
+ }>;
1
7
  export declare function runBeforeSubmitPromptHook(): Promise<void>;
8
+ export declare function handleShellGateHook(payload: Record<string, unknown>): Promise<import("../../core/gate-contract.js").GatePermissionResponse | {
9
+ permission: string;
10
+ user_message: string;
11
+ }>;
2
12
  export declare function runShellGateHook(): Promise<void>;
13
+ export declare function handleToolGateHook(eventName: string, payload: Record<string, unknown>): Promise<import("../../core/gate-contract.js").GatePermissionResponse | {
14
+ permission: string;
15
+ user_message: string;
16
+ } | {
17
+ permission: string;
18
+ user_message?: undefined;
19
+ }>;
3
20
  export declare function runToolGateHook(eventName: string): Promise<void>;
21
+ export declare function handleAuditHook(eventName: string, payload: Record<string, unknown>): Promise<{}>;
4
22
  export declare function runAuditHook(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,61 +65,119 @@ 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
  }
34
104
  function isFileMutationTool(toolName) {
35
105
  return toolName === 'Write' || toolName === 'StrReplace' || toolName === 'Delete';
36
106
  }
37
- export async function runBeforeSubmitPromptHook() {
107
+ export async function handleBeforeSubmitPromptHook(payload) {
38
108
  try {
39
- const payload = await readStdinJson();
40
109
  const prompt = String(payload.prompt ?? '');
41
- const ctx = await loadRuntimeContext(process.cwd());
110
+ const resolution = resolveCursorActionCwdDetails(payload);
111
+ if (isUnsafeGlobalHookFallback(resolution)) {
112
+ return {
113
+ continue: false,
114
+ user_message: GLOBAL_HOOK_WORKSPACE_MISSING_MESSAGE,
115
+ };
116
+ }
117
+ const ctx = await loadRuntimeContext(resolution.cwd);
42
118
  const deps = createDefaultGateRuntimeDeps();
43
- const result = await processApprovalPrompt(ctx, deps, prompt);
44
- jsonResponse({
119
+ const result = await processApprovalPromptWithRepoFallback(ctx, deps, prompt, payload, resolution);
120
+ return {
45
121
  continue: result.continue,
46
122
  ...(result.user_message ? { user_message: result.user_message } : {}),
47
123
  ...(result.replay ? { replay: result.replay } : {}),
48
- });
124
+ };
49
125
  }
50
126
  catch {
51
- jsonResponse({
127
+ return {
52
128
  continue: false,
53
129
  user_message: 'belay failed while processing approval state. Run belay doctor, then retry.',
54
- });
130
+ };
55
131
  }
56
132
  }
57
- export async function runShellGateHook() {
133
+ export async function runBeforeSubmitPromptHook() {
134
+ jsonResponse(await handleBeforeSubmitPromptHook(await readStdinJson()));
135
+ }
136
+ export async function handleShellGateHook(payload) {
58
137
  try {
59
- const payload = await readStdinJson();
60
138
  const command = String(payload.command ?? '').trim();
61
- const cwd = String(payload.cwd ?? process.cwd()).trim() || process.cwd();
139
+ const resolution = resolveCursorActionCwdDetails(payload);
140
+ if (isUnsafeGlobalHookFallback(resolution)) {
141
+ return {
142
+ permission: 'deny',
143
+ user_message: GLOBAL_HOOK_WORKSPACE_MISSING_MESSAGE,
144
+ };
145
+ }
146
+ const cwd = resolution.cwd;
62
147
  const ctx = await loadRuntimeContext(cwd);
63
148
  const deps = createDefaultGateRuntimeDeps();
64
149
  const verdict = await evaluateGatedAction(ctx, deps, {
65
150
  kind: 'shell',
66
151
  cwd,
67
152
  command,
153
+ payload,
68
154
  sourceEvent: 'beforeShellExecution',
69
155
  });
70
- jsonResponse(gateVerdictToCursorResponse(verdict));
156
+ return gateVerdictToCursorResponse(verdict);
71
157
  }
72
158
  catch {
73
- jsonResponse({
159
+ return {
74
160
  permission: 'deny',
75
161
  user_message: 'belay failed while classifying this shell command. Run belay doctor, then retry.',
76
- });
162
+ };
77
163
  }
78
164
  }
79
- export async function runToolGateHook(eventName) {
165
+ export async function runShellGateHook() {
166
+ jsonResponse(await handleShellGateHook(await readStdinJson()));
167
+ }
168
+ export async function handleToolGateHook(eventName, payload) {
80
169
  try {
81
- const payload = await readStdinJson();
82
- const cwd = process.cwd();
170
+ const toolName = String(payload.tool_name ?? '');
171
+ const resolution = resolveCursorToolActionCwdDetails(payload, process.cwd(), eventName, toolName);
172
+ if (isUnsafeGlobalHookFallback(resolution)) {
173
+ return {
174
+ permission: 'deny',
175
+ user_message: GLOBAL_HOOK_WORKSPACE_MISSING_MESSAGE,
176
+ };
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',
@@ -90,8 +185,7 @@ export async function runToolGateHook(eventName) {
90
185
  payload,
91
186
  sourceEvent: eventName,
92
187
  });
93
- jsonResponse(gateVerdictToCursorResponse(verdict));
94
- return;
188
+ return gateVerdictToCursorResponse(verdict);
95
189
  }
96
190
  if (toolName === 'Shell') {
97
191
  const verdict = await evaluateGatedAction(ctx, deps, {
@@ -101,8 +195,7 @@ export async function runToolGateHook(eventName) {
101
195
  toolName,
102
196
  sourceEvent: eventName,
103
197
  });
104
- jsonResponse(gateVerdictToCursorResponse(verdict));
105
- return;
198
+ return gateVerdictToCursorResponse(verdict);
106
199
  }
107
200
  if (isFileMutationTool(toolName)) {
108
201
  const verdict = await evaluateGatedAction(ctx, deps, {
@@ -112,8 +205,7 @@ export async function runToolGateHook(eventName) {
112
205
  toolName,
113
206
  sourceEvent: eventName,
114
207
  });
115
- jsonResponse(gateVerdictToCursorResponse(verdict));
116
- return;
208
+ return gateVerdictToCursorResponse(verdict);
117
209
  }
118
210
  if (payload.tool_name === 'Task') {
119
211
  const verdict = await evaluateGatedAction(ctx, deps, {
@@ -122,28 +214,37 @@ export async function runToolGateHook(eventName) {
122
214
  payload,
123
215
  sourceEvent: eventName,
124
216
  });
125
- jsonResponse(gateVerdictToCursorResponse(verdict));
126
- return;
217
+ return gateVerdictToCursorResponse(verdict);
127
218
  }
128
- jsonResponse({ permission: 'allow' });
219
+ return { permission: 'allow' };
129
220
  }
130
221
  catch {
131
- jsonResponse({
222
+ return {
132
223
  permission: 'deny',
133
224
  user_message: 'belay failed while classifying this tool action. Run belay doctor, then retry.',
134
- });
225
+ };
135
226
  }
136
227
  }
137
- export async function runAuditHook(eventName) {
228
+ export async function runToolGateHook(eventName) {
229
+ jsonResponse(await handleToolGateHook(eventName, await readStdinJson()));
230
+ }
231
+ export async function handleAuditHook(eventName, payload) {
138
232
  try {
139
- const payload = await readStdinJson();
140
- const ctx = await loadRuntimeContext(process.cwd());
233
+ const toolName = String(payload.tool_name ?? '');
234
+ const resolution = resolveCursorToolActionCwdDetails(payload, process.cwd(), eventName, toolName);
235
+ if (isUnsafeGlobalHookFallback(resolution)) {
236
+ return {};
237
+ }
238
+ const ctx = await loadRuntimeContext(resolution.cwd);
141
239
  const deps = createDefaultGateRuntimeDeps();
142
240
  await appendObservedAudit(ctx, deps, eventName, payload);
143
- jsonResponse({});
241
+ return {};
144
242
  }
145
243
  catch (error) {
146
244
  console.error('belay audit hook failed:', error instanceof Error ? error.message : String(error));
147
- jsonResponse({});
245
+ return {};
148
246
  }
149
247
  }
248
+ export async function runAuditHook(eventName) {
249
+ jsonResponse(await handleAuditHook(eventName, await readStdinJson()));
250
+ }
@@ -11,8 +11,13 @@ export interface ScopedPaths {
11
11
  skillsDir: string;
12
12
  commandsDir?: string;
13
13
  }
14
+ export declare function resolveTrustedWindowsPowerShellPath(systemRoot?: string | undefined): string;
14
15
  export declare function isPathInside(child: string, parent: string): boolean;
15
16
  export declare function buildRunnerInvocation(platform: NodeJS.Platform, hooksDir: string, repoRoot: string, hookScript: string, ...args: string[]): string;
17
+ export declare function buildAbsoluteRunnerInvocation(platform: NodeJS.Platform, hooksDir: string, hookScript: string, ...args: string[]): string;
18
+ export declare function buildLegacyBarePowerShellRunnerInvocation(platform: NodeJS.Platform, hooksDir: string, hookScript: string, ...args: string[]): string;
19
+ export declare function buildLegacyQuotedAbsoluteRunnerInvocation(platform: NodeJS.Platform, hooksDir: string, hookScript: string, ...args: string[]): string;
20
+ export declare function buildLegacyAbsoluteRunnerInvocation(platform: NodeJS.Platform, hooksDir: string, hookScript: string, ...args: string[]): string;
16
21
  export declare function resolveScopedPaths(layout: AdapterLayout, scope: InstallScope, repoRoot: string): ScopedPaths;
17
22
  export declare function resolveInstallScope(options: {
18
23
  scope?: InstallScope;
@@ -1,6 +1,50 @@
1
+ import { existsSync, realpathSync } from 'node:fs';
1
2
  import os from 'node:os';
2
3
  import path from 'node:path';
3
4
  import { isPathOutsideRoot } from '../../core/path-utils.js';
5
+ function canonicalizePotentialPath(inputPath) {
6
+ const unresolvedSegments = [];
7
+ let existingAncestor = path.resolve(inputPath);
8
+ while (!existsSync(existingAncestor)) {
9
+ const parent = path.dirname(existingAncestor);
10
+ if (parent === existingAncestor) {
11
+ break;
12
+ }
13
+ unresolvedSegments.unshift(path.basename(existingAncestor));
14
+ existingAncestor = parent;
15
+ }
16
+ return existsSync(existingAncestor)
17
+ ? path.join(realpathSync(existingAncestor), ...unresolvedSegments)
18
+ : path.resolve(inputPath);
19
+ }
20
+ function quotePowerShellLiteral(value) {
21
+ return `'${value.replaceAll("'", "''")}'`;
22
+ }
23
+ export function resolveTrustedWindowsPowerShellPath(systemRoot = process.env.SystemRoot || process.env.WINDIR) {
24
+ if (!systemRoot || !path.win32.isAbsolute(systemRoot)) {
25
+ throw new Error('A trusted Windows system root must be an absolute drive path.');
26
+ }
27
+ const hasControlCharacter = [...systemRoot].some((character) => character.charCodeAt(0) < 32);
28
+ if (systemRoot !== systemRoot.trim() || hasControlCharacter || /[%!?*"&|<>^]/.test(systemRoot)) {
29
+ throw new Error('The trusted Windows system root contains unsafe path or cmd.exe expansion characters.');
30
+ }
31
+ if (systemRoot.slice(2).includes(':')) {
32
+ throw new Error('The trusted Windows system root contains an invalid drive path.');
33
+ }
34
+ const normalizedRoot = path.win32.normalize(systemRoot);
35
+ if (!/^[A-Za-z]:\\$/.test(path.win32.parse(normalizedRoot).root)) {
36
+ throw new Error('A trusted Windows system root must be an absolute drive path.');
37
+ }
38
+ return path.win32.join(normalizedRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
39
+ }
40
+ function buildEncodedPowerShellRunnerInvocation(executable, runnerPath, hookScript, args) {
41
+ const encodedCommand = Buffer.from([
42
+ '&',
43
+ quotePowerShellLiteral(runnerPath),
44
+ ...[hookScript, ...args].map(quotePowerShellLiteral),
45
+ ].join(' '), 'utf16le').toString('base64');
46
+ return `${executable} -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encodedCommand}`;
47
+ }
4
48
  function agentHomeDir(adapter) {
5
49
  const home = os.homedir();
6
50
  if (adapter === 'cursor') {
@@ -38,6 +82,33 @@ export function buildRunnerInvocation(platform, hooksDir, repoRoot, hookScript,
38
82
  : runnerAbs;
39
83
  return [runnerRef, hookScript, ...args].join(' ');
40
84
  }
85
+ export function buildAbsoluteRunnerInvocation(platform, hooksDir, hookScript, ...args) {
86
+ const runnerFile = platform === 'win32' ? 'belay-runner.ps1' : 'belay-runner';
87
+ const canonicalHooksDir = canonicalizePotentialPath(hooksDir);
88
+ const runnerPath = path.join(canonicalHooksDir, runnerFile);
89
+ if (platform === 'win32') {
90
+ const powerShellPath = resolveTrustedWindowsPowerShellPath();
91
+ return buildEncodedPowerShellRunnerInvocation(`"${powerShellPath}"`, runnerPath, hookScript, args);
92
+ }
93
+ return [`'${runnerPath.replaceAll("'", "'\\''")}'`, hookScript, ...args].join(' ');
94
+ }
95
+ export function buildLegacyBarePowerShellRunnerInvocation(platform, hooksDir, hookScript, ...args) {
96
+ if (platform !== 'win32') {
97
+ return buildAbsoluteRunnerInvocation(platform, hooksDir, hookScript, ...args);
98
+ }
99
+ const runnerPath = path.join(canonicalizePotentialPath(hooksDir), 'belay-runner.ps1');
100
+ return buildEncodedPowerShellRunnerInvocation('powershell.exe', runnerPath, hookScript, args);
101
+ }
102
+ export function buildLegacyQuotedAbsoluteRunnerInvocation(platform, hooksDir, hookScript, ...args) {
103
+ const runnerFile = platform === 'win32' ? 'belay-runner.cmd' : 'belay-runner';
104
+ const runnerPath = path.join(canonicalizePotentialPath(hooksDir), runnerFile);
105
+ const quotedRunnerPath = platform === 'win32' ? `"${runnerPath}"` : `'${runnerPath.replaceAll("'", "'\\''")}'`;
106
+ return [quotedRunnerPath, hookScript, ...args].join(' ');
107
+ }
108
+ export function buildLegacyAbsoluteRunnerInvocation(platform, hooksDir, hookScript, ...args) {
109
+ const runnerFile = platform === 'win32' ? 'belay-runner.cmd' : 'belay-runner';
110
+ return [path.resolve(hooksDir, runnerFile), hookScript, ...args].join(' ');
111
+ }
41
112
  export function resolveScopedPaths(layout, scope, repoRoot) {
42
113
  const resolvedRepo = path.resolve(repoRoot);
43
114
  const adapter = layout.name;
@@ -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,10 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
904
910
  event: auditEvent,
905
911
  sourceEvent,
906
912
  kind,
913
+ repoLabel: path.basename(ctx.repoRoot),
914
+ ...(auditExtras.toolInvocationCorrelationId
915
+ ? { toolInvocationCorrelationId: auditExtras.toolInvocationCorrelationId }
916
+ : {}),
907
917
  fingerprint: result.fingerprint,
908
918
  summary: result.normalizedCommand ?? result.summary ?? '',
909
919
  assessment: result.assessment,
@@ -1398,12 +1408,26 @@ export function gateVerdictToCodexUserPromptResponse(verdict) {
1398
1408
  };
1399
1409
  }
1400
1410
  export async function appendObservedAudit(ctx, deps, eventName, payload) {
1411
+ const rawToolUseId = typeof payload.tool_use_id === 'string' ? payload.tool_use_id : undefined;
1412
+ const summaryPayload = redactToolInvocationId(payload, rawToolUseId);
1401
1413
  await deps.appendAudit(ctx, {
1402
1414
  event: eventName,
1403
1415
  kind: 'audit',
1404
1416
  verdict: 'allow',
1405
1417
  reason: 'observed',
1406
- summary: canonicalStringify(payload),
1418
+ ...(rawToolUseId
1419
+ ? { toolInvocationCorrelationId: toolInvocationCorrelationId(rawToolUseId) }
1420
+ : {}),
1421
+ ...(typeof summaryPayload.tool_name === 'string' ? { toolName: summaryPayload.tool_name } : {}),
1422
+ ...(typeof summaryPayload.failure_type === 'string'
1423
+ ? { failureType: summaryPayload.failure_type }
1424
+ : {}),
1425
+ ...(typeof summaryPayload.error_message === 'string'
1426
+ ? { errorMessage: summaryPayload.error_message }
1427
+ : {}),
1428
+ ...(typeof payload.duration === 'number' ? { durationMs: payload.duration } : {}),
1429
+ ...(typeof payload.is_interrupt === 'boolean' ? { isInterrupt: payload.is_interrupt } : {}),
1430
+ summary: canonicalStringify(summaryPayload),
1407
1431
  });
1408
1432
  }
1409
1433
  export { GateNormalizationError };