@guilz-dev/belay 0.9.3 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -3
- package/dist/adapters/codex/hooks.d.ts +1 -0
- package/dist/adapters/codex/hooks.js +3 -0
- package/dist/adapters/cursor/dispatcher-generation.d.ts +3 -0
- package/dist/adapters/cursor/dispatcher-generation.js +5 -0
- package/dist/adapters/cursor/hook-dispatch-entry.d.ts +10 -0
- package/dist/adapters/cursor/hook-dispatch-entry.js +137 -0
- package/dist/adapters/cursor/hook-router.d.ts +23 -0
- package/dist/adapters/cursor/hook-router.js +273 -0
- package/dist/adapters/cursor/hooks.d.ts +5 -1
- package/dist/adapters/cursor/hooks.js +112 -10
- package/dist/adapters/cursor/routing-config-trust.d.ts +1 -0
- package/dist/adapters/cursor/routing-config-trust.js +67 -0
- package/dist/adapters/cursor/routing-layout.d.ts +5 -0
- package/dist/adapters/cursor/routing-layout.js +29 -0
- package/dist/adapters/cursor/runtime-entry.d.ts +17 -0
- package/dist/adapters/cursor/runtime-entry.js +74 -49
- package/dist/adapters/layouts/scope.d.ts +5 -0
- package/dist/adapters/layouts/scope.js +71 -0
- package/dist/adapters/shared/gate-runtime.js +29 -17
- package/dist/bundle/claude-runtime.mjs +2428 -2072
- package/dist/bundle/codex-runtime.mjs +2440 -2084
- package/dist/bundle/cursor-dispatcher.mjs +517 -0
- package/dist/bundle/cursor-runtime.mjs +2521 -2133
- package/dist/cli.js +53 -4
- package/dist/commands/approval-token.d.ts +10 -0
- package/dist/commands/approval-token.js +26 -0
- package/dist/commands/audit.d.ts +2 -1
- package/dist/commands/audit.js +2 -2
- package/dist/commands/config.d.ts +1 -1
- package/dist/commands/config.js +28 -2
- package/dist/commands/doctor.js +149 -17
- package/dist/commands/dogfood-check.d.ts +3 -0
- package/dist/commands/dogfood-check.js +116 -0
- package/dist/commands/dogfood.d.ts +1 -0
- package/dist/commands/dogfood.js +4 -3
- package/dist/commands/health-snapshot.js +9 -4
- package/dist/commands/judge.js +2 -2
- package/dist/commands/recovery-checkpoints.d.ts +3 -3
- package/dist/commands/recovery-checkpoints.js +2 -11
- package/dist/config-io.d.ts +1 -0
- package/dist/config-io.js +19 -3
- package/dist/core/audit-legacy-archive.js +43 -25
- package/dist/core/classify-tool.js +16 -8
- package/dist/core/dogfood-environment.d.ts +8 -0
- package/dist/core/dogfood-environment.js +55 -0
- package/dist/core/effect-ir/argv-delegate.d.ts +9 -0
- package/dist/core/effect-ir/argv-delegate.js +42 -0
- package/dist/core/effect-ir/shell-lower/argv-delegate-gate.d.ts +3 -0
- package/dist/core/effect-ir/shell-lower/argv-delegate-gate.js +40 -0
- package/dist/core/effect-ir/shell-lower/augment.d.ts +4 -0
- package/dist/core/effect-ir/shell-lower/augment.js +97 -0
- package/dist/core/effect-ir/shell-lower/context.d.ts +10 -0
- package/dist/core/effect-ir/shell-lower/context.js +1 -0
- package/dist/core/effect-ir/shell-lower/decode-process.d.ts +9 -0
- package/dist/core/effect-ir/shell-lower/decode-process.js +201 -0
- package/dist/core/effect-ir/shell-lower/decoders/belay.d.ts +2 -0
- package/dist/core/effect-ir/shell-lower/decoders/belay.js +41 -0
- package/dist/core/effect-ir/shell-lower/decoders/builtins.d.ts +7 -0
- package/dist/core/effect-ir/shell-lower/decoders/builtins.js +194 -0
- package/dist/core/effect-ir/shell-lower/decoders/decode-package-exec.d.ts +4 -0
- package/dist/core/effect-ir/shell-lower/decoders/decode-package-exec.js +68 -0
- package/dist/core/effect-ir/shell-lower/decoders/docker.d.ts +3 -0
- package/dist/core/effect-ir/shell-lower/decoders/docker.js +43 -0
- package/dist/core/effect-ir/shell-lower/decoders/filesystem.d.ts +9 -0
- package/dist/core/effect-ir/shell-lower/decoders/filesystem.js +217 -0
- package/dist/core/effect-ir/shell-lower/decoders/prisma.d.ts +2 -0
- package/dist/core/effect-ir/shell-lower/decoders/prisma.js +54 -0
- package/dist/core/effect-ir/shell-lower/decoders/ruby.d.ts +11 -0
- package/dist/core/effect-ir/shell-lower/decoders/ruby.js +147 -0
- package/dist/core/effect-ir/shell-lower/decoders/toolchain.d.ts +7 -0
- package/dist/core/effect-ir/shell-lower/decoders/toolchain.js +155 -0
- package/dist/core/effect-ir/shell-lower/requirement.d.ts +12 -0
- package/dist/core/effect-ir/shell-lower/requirement.js +65 -0
- package/dist/core/effect-ir/shell-lower/segment.d.ts +11 -0
- package/dist/core/effect-ir/shell-lower/segment.js +82 -0
- package/dist/core/effect-ir/shell-lower/tokens.d.ts +26 -0
- package/dist/core/effect-ir/shell-lower/tokens.js +183 -0
- package/dist/core/effect-ir/shell-lower.d.ts +2 -7
- package/dist/core/effect-ir/shell-lower.js +60 -1502
- package/dist/core/egress-approval.js +0 -18
- package/dist/core/glob.js +32 -16
- package/dist/core/integrity.d.ts +2 -2
- package/dist/core/integrity.js +52 -12
- package/dist/core/notify.d.ts +8 -2
- package/dist/core/notify.js +63 -7
- package/dist/core/recovery/operator-guidance.js +4 -4
- package/dist/core/replay-scrub.d.ts +2 -0
- package/dist/core/replay-scrub.js +8 -0
- package/dist/core/repo-config-trust.d.ts +31 -0
- package/dist/core/repo-config-trust.js +152 -0
- package/dist/core/shell-substitution.js +2 -2
- package/dist/core/shell-unparseable.js +3 -1
- package/dist/corpus/benign-probe-cores.d.ts +1 -1
- package/dist/corpus/benign-probe-cores.js +0 -1
- package/dist/defaults.d.ts +7 -3
- package/dist/defaults.js +29 -47
- package/dist/installer/bootstrap.d.ts +1 -0
- package/dist/installer/bootstrap.js +2 -2
- package/dist/installer/runtime-artifacts.js +43 -13
- package/dist/installer/scope-config.js +2 -2
- package/dist/installer.js +48 -13
- package/dist/node-resolution.d.ts +1 -0
- package/dist/node-resolution.js +61 -0
- package/dist/templates.d.ts +7 -4
- package/dist/templates.js +42 -4
- package/dist/types.d.ts +20 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -1
|
@@ -1,25 +1,61 @@
|
|
|
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
|
|
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
|
+
const legacyMatchedToolNames = ['Task', 'Write', 'StrReplace', 'Delete'];
|
|
20
|
+
return [
|
|
21
|
+
...entries,
|
|
22
|
+
...entries.flatMap(({ event, definition }) => event === 'preToolUse' && definition.matcher === undefined
|
|
23
|
+
? legacyMatchedToolNames.map((matcher) => ({
|
|
24
|
+
event,
|
|
25
|
+
definition: { ...definition, matcher },
|
|
26
|
+
}))
|
|
27
|
+
: []),
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
function variantsForDefinition(legacyEntries, event, definition) {
|
|
31
|
+
return [
|
|
32
|
+
definition,
|
|
33
|
+
...legacyEntries
|
|
34
|
+
.filter((entry) => entry.event === event &&
|
|
35
|
+
(entry.definition.matcher === definition.matcher ||
|
|
36
|
+
(event === 'preToolUse' && definition.matcher === undefined)))
|
|
37
|
+
.map((entry) => entry.definition),
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
function mergeHookEntry(current, expected, managedVariants, placement) {
|
|
6
41
|
const entries = Array.isArray(current) ? [...current] : [];
|
|
7
|
-
const filtered = entries.filter((entry) => !entryMatches(entry,
|
|
42
|
+
const filtered = entries.filter((entry) => !managedVariants.some((variant) => entryMatches(entry, variant)));
|
|
8
43
|
if (placement === 'prepend') {
|
|
9
44
|
return [expected, ...filtered];
|
|
10
45
|
}
|
|
11
46
|
return [...filtered, expected];
|
|
12
47
|
}
|
|
13
|
-
/**
|
|
48
|
+
/** Legacy managed Shell preToolUse gate matcher (upgrade/uninstall migration only). */
|
|
14
49
|
export function managedShellPreToolUseEntry(platform, hooksDir, repoRoot) {
|
|
15
50
|
const managed = getManagedHookEntries(platform, hooksDir, repoRoot);
|
|
16
|
-
const referencePreToolUse = managed.find((entry) => entry.event === 'preToolUse' && entry.definition.matcher ===
|
|
51
|
+
const referencePreToolUse = managed.find((entry) => entry.event === 'preToolUse' && entry.definition.matcher === undefined)?.definition;
|
|
17
52
|
if (!referencePreToolUse) {
|
|
18
53
|
throw new Error('managed hook definitions missing for Shell preToolUse gate');
|
|
19
54
|
}
|
|
20
55
|
return {
|
|
21
56
|
command: referencePreToolUse.command,
|
|
22
57
|
matcher: 'Shell',
|
|
58
|
+
failClosed: true,
|
|
23
59
|
};
|
|
24
60
|
}
|
|
25
61
|
/** @deprecated Use {@link managedShellPreToolUseEntry}. */
|
|
@@ -30,20 +66,19 @@ export function stripCursorHooksFile(current, platform, hooksDir, repoRoot) {
|
|
|
30
66
|
hooks: { ...current.hooks },
|
|
31
67
|
};
|
|
32
68
|
const managedEntries = getManagedHookEntries(platform, hooksDir, repoRoot);
|
|
69
|
+
const legacyEntries = legacyManagedEntries(platform, hooksDir, repoRoot);
|
|
33
70
|
for (const { event, definition } of managedEntries) {
|
|
34
71
|
const entries = next.hooks[event];
|
|
35
72
|
if (!Array.isArray(entries)) {
|
|
36
73
|
continue;
|
|
37
74
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
matcher: definition.matcher,
|
|
41
|
-
}));
|
|
75
|
+
const variants = variantsForDefinition(legacyEntries, event, definition);
|
|
76
|
+
next.hooks[event] = entries.filter((entry) => !variants.some((variant) => entryMatches(entry, variant)));
|
|
42
77
|
if (next.hooks[event].length === 0) {
|
|
43
78
|
delete next.hooks[event];
|
|
44
79
|
}
|
|
45
80
|
}
|
|
46
|
-
return next;
|
|
81
|
+
return stripLegacyManagedShellPreToolUseGates(next, platform, hooksDir, repoRoot);
|
|
47
82
|
}
|
|
48
83
|
export function mergeCursorHooksFile(current, platform, hooksDir, repoRoot) {
|
|
49
84
|
const next = {
|
|
@@ -51,14 +86,74 @@ export function mergeCursorHooksFile(current, platform, hooksDir, repoRoot) {
|
|
|
51
86
|
hooks: { ...current.hooks },
|
|
52
87
|
};
|
|
53
88
|
const managedEntries = getManagedHookEntries(platform, hooksDir, repoRoot);
|
|
89
|
+
const legacyEntries = legacyManagedEntries(platform, hooksDir, repoRoot);
|
|
54
90
|
for (const { event, definition } of managedEntries) {
|
|
91
|
+
const variants = variantsForDefinition(legacyEntries, event, definition);
|
|
55
92
|
next.hooks[event] = mergeHookEntry(next.hooks[event], {
|
|
56
93
|
command: definition.command,
|
|
57
94
|
matcher: definition.matcher,
|
|
58
|
-
|
|
95
|
+
failClosed: definition.failClosed,
|
|
96
|
+
}, variants, definition.placement);
|
|
97
|
+
}
|
|
98
|
+
return stripLegacyManagedShellPreToolUseGates(next, platform, hooksDir, repoRoot);
|
|
99
|
+
}
|
|
100
|
+
export function legacyManagedShellPreToolUseVariants(platform, hooksDir, repoRoot) {
|
|
101
|
+
const shellEntry = managedShellPreToolUseEntry(platform, hooksDir, repoRoot);
|
|
102
|
+
const legacyEntries = legacyManagedEntries(platform, hooksDir, repoRoot);
|
|
103
|
+
const commands = new Set([shellEntry.command]);
|
|
104
|
+
for (const entry of legacyEntries) {
|
|
105
|
+
if (entry.event === 'preToolUse') {
|
|
106
|
+
commands.add(entry.definition.command);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return [...commands].map((command) => ({
|
|
110
|
+
command,
|
|
111
|
+
matcher: 'Shell',
|
|
112
|
+
failClosed: true,
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
function stripLegacyManagedShellPreToolUseGates(hooks, platform, hooksDir, repoRoot) {
|
|
116
|
+
const variants = legacyManagedShellPreToolUseVariants(platform, hooksDir, repoRoot);
|
|
117
|
+
const preToolUse = hooks.hooks.preToolUse;
|
|
118
|
+
if (!Array.isArray(preToolUse) || variants.length === 0) {
|
|
119
|
+
return hooks;
|
|
120
|
+
}
|
|
121
|
+
const filtered = preToolUse.filter((entry) => !variants.some((variant) => entryMatches(entry, variant)));
|
|
122
|
+
if (filtered.length === preToolUse.length) {
|
|
123
|
+
return hooks;
|
|
124
|
+
}
|
|
125
|
+
const next = {
|
|
126
|
+
version: hooks.version || 1,
|
|
127
|
+
hooks: { ...hooks.hooks },
|
|
128
|
+
};
|
|
129
|
+
if (filtered.length === 0) {
|
|
130
|
+
delete next.hooks.preToolUse;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
next.hooks.preToolUse = filtered;
|
|
59
134
|
}
|
|
60
135
|
return next;
|
|
61
136
|
}
|
|
137
|
+
export function hasLegacyCursorDoubleShellGates(hooks, platform, hooksDir, repoRoot) {
|
|
138
|
+
const shellVariants = legacyManagedShellPreToolUseVariants(platform, hooksDir, repoRoot);
|
|
139
|
+
const preToolUse = hooks.hooks.preToolUse;
|
|
140
|
+
const beforeShell = hooks.hooks.beforeShellExecution;
|
|
141
|
+
if (!Array.isArray(preToolUse) || !Array.isArray(beforeShell)) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
const hasLegacyShellPreToolUse = preToolUse.some((entry) => shellVariants.some((variant) => entryMatches(entry, variant)));
|
|
145
|
+
if (!hasLegacyShellPreToolUse) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
const managedEntries = getManagedHookEntries(platform, hooksDir, repoRoot);
|
|
149
|
+
const shellManaged = managedEntries.find((entry) => entry.event === 'beforeShellExecution');
|
|
150
|
+
if (!shellManaged) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
const shellVariantsForBefore = variantsForDefinition(legacyManagedEntries(platform, hooksDir, repoRoot), 'beforeShellExecution', shellManaged.definition);
|
|
154
|
+
return beforeShell.some((entry) => shellVariantsForBefore.some((variant) => entryMatches(entry, variant)));
|
|
155
|
+
}
|
|
156
|
+
/** @deprecated Use {@link hasLegacyCursorDoubleShellGates}. */
|
|
62
157
|
export function hasDuplicateCursorShellGates(hooks, platform, hooksDir, repoRoot) {
|
|
63
158
|
const shellEntry = managedShellPreToolUseEntry(platform, hooksDir, repoRoot);
|
|
64
159
|
const preToolUse = hooks.hooks.preToolUse;
|
|
@@ -68,3 +163,10 @@ export function hasDuplicateCursorShellGates(hooks, platform, hooksDir, repoRoot
|
|
|
68
163
|
const shellMatches = preToolUse.filter((entry) => entryMatches(entry, shellEntry));
|
|
69
164
|
return shellMatches.length > 1;
|
|
70
165
|
}
|
|
166
|
+
export function hasManagedCursorHookEntries(hooks, platform, hooksDir, repoRoot) {
|
|
167
|
+
const managedEntries = [
|
|
168
|
+
...getManagedHookEntries(platform, hooksDir, repoRoot),
|
|
169
|
+
...legacyManagedEntries(platform, hooksDir, repoRoot),
|
|
170
|
+
];
|
|
171
|
+
return managedEntries.some(({ event, definition }) => hooks.hooks[event]?.some((entry) => entryMatches(entry, definition)));
|
|
172
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isTrustedCursorRoutingConfig(repoRoot: string, rawConfig: unknown): boolean;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
function canonicalStringify(value) {
|
|
5
|
+
if (value === null || typeof value !== 'object') {
|
|
6
|
+
return JSON.stringify(value);
|
|
7
|
+
}
|
|
8
|
+
if (Array.isArray(value)) {
|
|
9
|
+
return `[${value.map((item) => canonicalStringify(item)).join(',')}]`;
|
|
10
|
+
}
|
|
11
|
+
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
|
|
12
|
+
return `{${entries.map(([key, child]) => `${JSON.stringify(key)}:${canonicalStringify(child)}`).join(',')}}`;
|
|
13
|
+
}
|
|
14
|
+
function hashValue(value) {
|
|
15
|
+
return createHash('sha256').update(value).digest('hex');
|
|
16
|
+
}
|
|
17
|
+
function defaultControlPlaneDir() {
|
|
18
|
+
if (process.platform === 'win32' && process.env.APPDATA?.trim()) {
|
|
19
|
+
return path.join(process.env.APPDATA.trim(), 'agent-belay');
|
|
20
|
+
}
|
|
21
|
+
const base = process.env.XDG_CONFIG_HOME?.trim() || path.join(process.env.HOME ?? '', '.config');
|
|
22
|
+
return path.join(base, 'agent-belay');
|
|
23
|
+
}
|
|
24
|
+
function canonicalRepoRoot(repoRoot) {
|
|
25
|
+
try {
|
|
26
|
+
return realpathSync(repoRoot);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return path.resolve(repoRoot);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function isTrustedCursorRoutingConfig(repoRoot, rawConfig) {
|
|
33
|
+
const canonicalRoot = canonicalRepoRoot(repoRoot);
|
|
34
|
+
const identity = hashValue(`${canonicalRoot}\u0000cursor`);
|
|
35
|
+
const recordPath = path.join(defaultControlPlaneDir(), 'config-trust', `${identity}.json`);
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(readFileSync(recordPath, 'utf8'));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const record = parsed;
|
|
47
|
+
const expectedKeys = new Set([
|
|
48
|
+
'schemaVersion',
|
|
49
|
+
'repoRoot',
|
|
50
|
+
'adapter',
|
|
51
|
+
'repoConfigFingerprint',
|
|
52
|
+
'trustedAt',
|
|
53
|
+
]);
|
|
54
|
+
if (Object.keys(record).length !== expectedKeys.size ||
|
|
55
|
+
!Object.keys(record).every((key) => expectedKeys.has(key)) ||
|
|
56
|
+
record.schemaVersion !== 1 ||
|
|
57
|
+
record.adapter !== 'cursor' ||
|
|
58
|
+
typeof record.repoRoot !== 'string' ||
|
|
59
|
+
canonicalRepoRoot(record.repoRoot) !== canonicalRoot ||
|
|
60
|
+
typeof record.repoConfigFingerprint !== 'string' ||
|
|
61
|
+
!/^[a-f0-9]{64}$/.test(record.repoConfigFingerprint) ||
|
|
62
|
+
typeof record.trustedAt !== 'string' ||
|
|
63
|
+
!Number.isFinite(Date.parse(record.trustedAt))) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return record.repoConfigFingerprint === hashValue(canonicalStringify(rawConfig));
|
|
67
|
+
}
|
|
@@ -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,5 +1,22 @@
|
|
|
1
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
|
+
}>;
|
|
2
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
|
+
}>;
|
|
3
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?: undefined;
|
|
16
|
+
} | {
|
|
17
|
+
permission: string;
|
|
18
|
+
user_message: string;
|
|
19
|
+
}>;
|
|
4
20
|
export declare function runToolGateHook(eventName: string): Promise<void>;
|
|
21
|
+
export declare function handleAuditHook(eventName: string, payload: Record<string, unknown>): Promise<{}>;
|
|
5
22
|
export declare function runAuditHook(eventName: string): Promise<void>;
|
|
@@ -3,6 +3,7 @@ import process from 'node:process';
|
|
|
3
3
|
import { approvalCommandMatch } from '../../core/approval.js';
|
|
4
4
|
import { findApprovalRepoRoots, formatAmbiguousApprovalRepoMessage, } from '../../core/approval-repo-lookup.js';
|
|
5
5
|
import { DEFAULT_CONFIG_V4 } from '../../core/config.js';
|
|
6
|
+
import { isRepoConfigTrustError } from '../../core/repo-config-trust.js';
|
|
6
7
|
import { cursorLayout } from '../layouts/cursor.js';
|
|
7
8
|
import { appendObservedAudit, createDefaultGateRuntimeDeps, evaluateGatedAction, gateVerdictToCursorResponse, processApprovalPrompt, resolveGateConfig, } from '../shared/gate-runtime.js';
|
|
8
9
|
import { findRepoRoot } from '../shared/repo-root.js';
|
|
@@ -104,45 +105,53 @@ function isSubagentEvent(payload, eventName) {
|
|
|
104
105
|
function isFileMutationTool(toolName) {
|
|
105
106
|
return toolName === 'Write' || toolName === 'StrReplace' || toolName === 'Delete';
|
|
106
107
|
}
|
|
107
|
-
|
|
108
|
+
function isCursorPreToolUseEvent(eventName) {
|
|
109
|
+
return eventName === 'preToolUse' || eventName === 'PreToolUse';
|
|
110
|
+
}
|
|
111
|
+
export async function handleBeforeSubmitPromptHook(payload) {
|
|
108
112
|
try {
|
|
109
|
-
const payload = await readStdinJson();
|
|
110
113
|
const prompt = String(payload.prompt ?? '');
|
|
111
114
|
const resolution = resolveCursorActionCwdDetails(payload);
|
|
112
115
|
if (isUnsafeGlobalHookFallback(resolution)) {
|
|
113
|
-
|
|
116
|
+
return {
|
|
114
117
|
continue: false,
|
|
115
118
|
user_message: GLOBAL_HOOK_WORKSPACE_MISSING_MESSAGE,
|
|
116
|
-
}
|
|
117
|
-
return;
|
|
119
|
+
};
|
|
118
120
|
}
|
|
119
121
|
const ctx = await loadRuntimeContext(resolution.cwd);
|
|
120
122
|
const deps = createDefaultGateRuntimeDeps();
|
|
121
123
|
const result = await processApprovalPromptWithRepoFallback(ctx, deps, prompt, payload, resolution);
|
|
122
|
-
|
|
124
|
+
return {
|
|
123
125
|
continue: result.continue,
|
|
124
126
|
...(result.user_message ? { user_message: result.user_message } : {}),
|
|
125
127
|
...(result.replay ? { replay: result.replay } : {}),
|
|
126
|
-
}
|
|
128
|
+
};
|
|
127
129
|
}
|
|
128
|
-
catch {
|
|
129
|
-
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (isRepoConfigTrustError(error)) {
|
|
132
|
+
return {
|
|
133
|
+
continue: false,
|
|
134
|
+
user_message: error.message,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
130
138
|
continue: false,
|
|
131
139
|
user_message: 'belay failed while processing approval state. Run belay doctor, then retry.',
|
|
132
|
-
}
|
|
140
|
+
};
|
|
133
141
|
}
|
|
134
142
|
}
|
|
135
|
-
export async function
|
|
143
|
+
export async function runBeforeSubmitPromptHook() {
|
|
144
|
+
jsonResponse(await handleBeforeSubmitPromptHook(await readStdinJson()));
|
|
145
|
+
}
|
|
146
|
+
export async function handleShellGateHook(payload) {
|
|
136
147
|
try {
|
|
137
|
-
const payload = await readStdinJson();
|
|
138
148
|
const command = String(payload.command ?? '').trim();
|
|
139
149
|
const resolution = resolveCursorActionCwdDetails(payload);
|
|
140
150
|
if (isUnsafeGlobalHookFallback(resolution)) {
|
|
141
|
-
|
|
151
|
+
return {
|
|
142
152
|
permission: 'deny',
|
|
143
153
|
user_message: GLOBAL_HOOK_WORKSPACE_MISSING_MESSAGE,
|
|
144
|
-
}
|
|
145
|
-
return;
|
|
154
|
+
};
|
|
146
155
|
}
|
|
147
156
|
const cwd = resolution.cwd;
|
|
148
157
|
const ctx = await loadRuntimeContext(cwd);
|
|
@@ -154,26 +163,36 @@ export async function runShellGateHook() {
|
|
|
154
163
|
payload,
|
|
155
164
|
sourceEvent: 'beforeShellExecution',
|
|
156
165
|
});
|
|
157
|
-
|
|
166
|
+
return gateVerdictToCursorResponse(verdict);
|
|
158
167
|
}
|
|
159
|
-
catch {
|
|
160
|
-
|
|
168
|
+
catch (error) {
|
|
169
|
+
if (isRepoConfigTrustError(error)) {
|
|
170
|
+
return {
|
|
171
|
+
permission: 'deny',
|
|
172
|
+
user_message: error.message,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
161
176
|
permission: 'deny',
|
|
162
177
|
user_message: 'belay failed while classifying this shell command. Run belay doctor, then retry.',
|
|
163
|
-
}
|
|
178
|
+
};
|
|
164
179
|
}
|
|
165
180
|
}
|
|
166
|
-
export async function
|
|
181
|
+
export async function runShellGateHook() {
|
|
182
|
+
jsonResponse(await handleShellGateHook(await readStdinJson()));
|
|
183
|
+
}
|
|
184
|
+
export async function handleToolGateHook(eventName, payload) {
|
|
167
185
|
try {
|
|
168
|
-
const payload = await readStdinJson();
|
|
169
186
|
const toolName = String(payload.tool_name ?? '');
|
|
187
|
+
if (isCursorPreToolUseEvent(eventName) && toolName === 'Shell') {
|
|
188
|
+
return { permission: 'allow' };
|
|
189
|
+
}
|
|
170
190
|
const resolution = resolveCursorToolActionCwdDetails(payload, process.cwd(), eventName, toolName);
|
|
171
191
|
if (isUnsafeGlobalHookFallback(resolution)) {
|
|
172
|
-
|
|
192
|
+
return {
|
|
173
193
|
permission: 'deny',
|
|
174
194
|
user_message: GLOBAL_HOOK_WORKSPACE_MISSING_MESSAGE,
|
|
175
|
-
}
|
|
176
|
-
return;
|
|
195
|
+
};
|
|
177
196
|
}
|
|
178
197
|
const cwd = resolution.cwd;
|
|
179
198
|
const ctx = await loadRuntimeContext(cwd);
|
|
@@ -185,66 +204,72 @@ export async function runToolGateHook(eventName) {
|
|
|
185
204
|
payload,
|
|
186
205
|
sourceEvent: eventName,
|
|
187
206
|
});
|
|
188
|
-
|
|
189
|
-
return;
|
|
207
|
+
return gateVerdictToCursorResponse(verdict);
|
|
190
208
|
}
|
|
191
|
-
if (toolName
|
|
209
|
+
if (isFileMutationTool(toolName)) {
|
|
192
210
|
const verdict = await evaluateGatedAction(ctx, deps, {
|
|
193
|
-
kind: '
|
|
211
|
+
kind: 'tool',
|
|
194
212
|
cwd,
|
|
195
213
|
payload,
|
|
196
214
|
toolName,
|
|
197
215
|
sourceEvent: eventName,
|
|
198
216
|
});
|
|
199
|
-
|
|
200
|
-
return;
|
|
217
|
+
return gateVerdictToCursorResponse(verdict);
|
|
201
218
|
}
|
|
202
|
-
if (
|
|
219
|
+
if (payload.tool_name === 'Task') {
|
|
203
220
|
const verdict = await evaluateGatedAction(ctx, deps, {
|
|
204
|
-
kind: '
|
|
221
|
+
kind: 'subagent',
|
|
205
222
|
cwd,
|
|
206
223
|
payload,
|
|
207
|
-
toolName,
|
|
208
224
|
sourceEvent: eventName,
|
|
209
225
|
});
|
|
210
|
-
|
|
211
|
-
return;
|
|
226
|
+
return gateVerdictToCursorResponse(verdict);
|
|
212
227
|
}
|
|
213
|
-
if (
|
|
228
|
+
if (isCursorPreToolUseEvent(eventName)) {
|
|
214
229
|
const verdict = await evaluateGatedAction(ctx, deps, {
|
|
215
|
-
kind: '
|
|
230
|
+
kind: 'tool',
|
|
216
231
|
cwd,
|
|
217
232
|
payload,
|
|
233
|
+
toolName,
|
|
218
234
|
sourceEvent: eventName,
|
|
219
235
|
});
|
|
220
|
-
|
|
221
|
-
return;
|
|
236
|
+
return gateVerdictToCursorResponse(verdict);
|
|
222
237
|
}
|
|
223
|
-
|
|
238
|
+
return { permission: 'allow' };
|
|
224
239
|
}
|
|
225
|
-
catch {
|
|
226
|
-
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (isRepoConfigTrustError(error)) {
|
|
242
|
+
return {
|
|
243
|
+
permission: 'deny',
|
|
244
|
+
user_message: error.message,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
227
248
|
permission: 'deny',
|
|
228
249
|
user_message: 'belay failed while classifying this tool action. Run belay doctor, then retry.',
|
|
229
|
-
}
|
|
250
|
+
};
|
|
230
251
|
}
|
|
231
252
|
}
|
|
232
|
-
export async function
|
|
253
|
+
export async function runToolGateHook(eventName) {
|
|
254
|
+
jsonResponse(await handleToolGateHook(eventName, await readStdinJson()));
|
|
255
|
+
}
|
|
256
|
+
export async function handleAuditHook(eventName, payload) {
|
|
233
257
|
try {
|
|
234
|
-
const payload = await readStdinJson();
|
|
235
258
|
const toolName = String(payload.tool_name ?? '');
|
|
236
259
|
const resolution = resolveCursorToolActionCwdDetails(payload, process.cwd(), eventName, toolName);
|
|
237
260
|
if (isUnsafeGlobalHookFallback(resolution)) {
|
|
238
|
-
|
|
239
|
-
return;
|
|
261
|
+
return {};
|
|
240
262
|
}
|
|
241
263
|
const ctx = await loadRuntimeContext(resolution.cwd);
|
|
242
264
|
const deps = createDefaultGateRuntimeDeps();
|
|
243
265
|
await appendObservedAudit(ctx, deps, eventName, payload);
|
|
244
|
-
|
|
266
|
+
return {};
|
|
245
267
|
}
|
|
246
268
|
catch (error) {
|
|
247
269
|
console.error('belay audit hook failed:', error instanceof Error ? error.message : String(error));
|
|
248
|
-
|
|
270
|
+
return {};
|
|
249
271
|
}
|
|
250
272
|
}
|
|
273
|
+
export async function runAuditHook(eventName) {
|
|
274
|
+
jsonResponse(await handleAuditHook(eventName, await readStdinJson()));
|
|
275
|
+
}
|
|
@@ -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;
|