@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
|
@@ -2,11 +2,12 @@ import path from 'node:path';
|
|
|
2
2
|
import { findCommandSubstitutions, findStructuralCommandSubstitutions, } from '../shell-substitution.js';
|
|
3
3
|
import { commandKey, tokenizeShell } from '../shell-tokenizer.js';
|
|
4
4
|
import { detectUnparseableShell } from '../shell-unparseable.js';
|
|
5
|
+
import { decodeDockerComposeRunValues } from './docker-compose-run.js';
|
|
6
|
+
import { decodeRecursiveInvocation, shellTokensFromValues, } from './recursive-invocation.js';
|
|
5
7
|
const ENV_PREFIX_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=(?:'[^']*'|"[^"]*"|\S+)$/;
|
|
6
8
|
const MAX_WRAPPER_PEEL_DEPTH = 32;
|
|
7
9
|
const SHELL_INTERPRETERS = new Set(['bash', 'sh', 'zsh', 'dash', 'fish']);
|
|
8
10
|
const CODE_INTERPRETERS = new Set(['python', 'python3', 'node', 'ruby', 'perl', 'osascript']);
|
|
9
|
-
const SCRIPT_FLAGS = new Set(['-c', '-lc', '-e', '--eval']);
|
|
10
11
|
const INTERPRETER_SCRIPT_EXTENSIONS = new Set([
|
|
11
12
|
'.js',
|
|
12
13
|
'.mjs',
|
|
@@ -30,10 +31,6 @@ export function peelTransparentWrappers(tokens) {
|
|
|
30
31
|
let encounteredXargs = false;
|
|
31
32
|
let peelDepth = 0;
|
|
32
33
|
while (current.length > 0) {
|
|
33
|
-
if (peelDepth >= MAX_WRAPPER_PEEL_DEPTH) {
|
|
34
|
-
return { tokens: current, xargsStdinOpaque: false, encounteredXargs, opaque: true };
|
|
35
|
-
}
|
|
36
|
-
peelDepth += 1;
|
|
37
34
|
while (current.length > 0 && ENV_PREFIX_PATTERN.test(current[0] ?? '')) {
|
|
38
35
|
current.shift();
|
|
39
36
|
}
|
|
@@ -43,6 +40,10 @@ export function peelTransparentWrappers(tokens) {
|
|
|
43
40
|
const head = normalizeHead(current[0] ?? '');
|
|
44
41
|
if (head === 'xargs') {
|
|
45
42
|
encounteredXargs = true;
|
|
43
|
+
if (peelDepth >= MAX_WRAPPER_PEEL_DEPTH) {
|
|
44
|
+
return { tokens: current, xargsStdinOpaque: false, encounteredXargs, opaque: true };
|
|
45
|
+
}
|
|
46
|
+
peelDepth += 1;
|
|
46
47
|
const wrapper = peelXargsWrapper(current);
|
|
47
48
|
if (wrapper.kind === 'opaque') {
|
|
48
49
|
xargsStdinOpaque = current.length === 1;
|
|
@@ -60,6 +61,10 @@ export function peelTransparentWrappers(tokens) {
|
|
|
60
61
|
if (!wrapper) {
|
|
61
62
|
break;
|
|
62
63
|
}
|
|
64
|
+
if (peelDepth >= MAX_WRAPPER_PEEL_DEPTH) {
|
|
65
|
+
return { tokens: current, xargsStdinOpaque: false, encounteredXargs, opaque: true };
|
|
66
|
+
}
|
|
67
|
+
peelDepth += 1;
|
|
63
68
|
if (wrapper.kind === 'opaque') {
|
|
64
69
|
return { tokens: current, xargsStdinOpaque: false, encounteredXargs, opaque: true };
|
|
65
70
|
}
|
|
@@ -300,31 +305,23 @@ export function extractRecursiveScript(tokens) {
|
|
|
300
305
|
return null;
|
|
301
306
|
}
|
|
302
307
|
const head = normalizeHead(filtered[0] ?? '');
|
|
303
|
-
const second = filtered[1] ?? '';
|
|
304
308
|
if (head === 'eval') {
|
|
305
309
|
const body = filtered.slice(1).join(' ').trim();
|
|
306
310
|
return body || null;
|
|
307
311
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
.slice(2)
|
|
322
|
-
.join(' ')
|
|
323
|
-
.replace(/^['"]|['"]$/g, '')
|
|
324
|
-
.trim();
|
|
325
|
-
return body || null;
|
|
326
|
-
}
|
|
327
|
-
return null;
|
|
312
|
+
const invocation = decodeRecursiveInvocation(shellTokensFromValues(filtered, { detectExpansion: false }));
|
|
313
|
+
return invocation.kind === 'static' ? invocation.script || null : null;
|
|
314
|
+
}
|
|
315
|
+
export function decodeRecursiveInvocationTokens(tokens) {
|
|
316
|
+
const values = tokens.map((token) => token.value);
|
|
317
|
+
const { tokens: filtered, opaque } = peelTransparentWrappers(values);
|
|
318
|
+
if (opaque)
|
|
319
|
+
return { kind: 'none' };
|
|
320
|
+
return decodeRecursiveInvocation(tokens.slice(values.length - filtered.length));
|
|
321
|
+
}
|
|
322
|
+
export function extractDockerComposeRunScript(tokens) {
|
|
323
|
+
const invocation = decodeDockerComposeRunValues(tokens);
|
|
324
|
+
return invocation.kind === 'recursive' ? invocation.script || null : null;
|
|
328
325
|
}
|
|
329
326
|
/**
|
|
330
327
|
* True when a recursive script is evaluated from a command argument rather
|
|
@@ -340,8 +337,7 @@ export function isDynamicRecursiveEvaluation(tokens) {
|
|
|
340
337
|
if (head === 'eval') {
|
|
341
338
|
return true;
|
|
342
339
|
}
|
|
343
|
-
return ((
|
|
344
|
-
filtered.some((token) => SCRIPT_FLAGS.has(token)));
|
|
340
|
+
return decodeRecursiveInvocation(shellTokensFromValues(filtered)).kind !== 'none';
|
|
345
341
|
}
|
|
346
342
|
export function isCommandInspection(tokens) {
|
|
347
343
|
return (normalizeHead(tokens[0] ?? '') === 'command' && peelCommandWrapper(tokens).kind === 'preserve');
|
|
@@ -358,8 +354,7 @@ export function isBareInterpreter(tokens) {
|
|
|
358
354
|
if (!SHELL_INTERPRETERS.has(head) && !CODE_INTERPRETERS.has(head)) {
|
|
359
355
|
return false;
|
|
360
356
|
}
|
|
361
|
-
|
|
362
|
-
if (hasScriptFlag) {
|
|
357
|
+
if (decodeRecursiveInvocation(shellTokensFromValues(peeled)).kind !== 'none') {
|
|
363
358
|
return false;
|
|
364
359
|
}
|
|
365
360
|
const args = peeled.slice(1);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ShellToken } from '../shell-tokenizer.js';
|
|
2
|
+
export type RecursiveInvocation = {
|
|
3
|
+
kind: 'static';
|
|
4
|
+
interpreter: string;
|
|
5
|
+
script: string;
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'dynamic';
|
|
8
|
+
interpreter: string;
|
|
9
|
+
signal: 'shell.script_expanded';
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'none';
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'indeterminate';
|
|
14
|
+
interpreter: string;
|
|
15
|
+
signal: 'shell.interpreter_argv_incomplete' | 'shell.interpreter_option_unknown';
|
|
16
|
+
};
|
|
17
|
+
export declare function decodeRecursiveInvocation(tokens: readonly ShellToken[]): RecursiveInvocation;
|
|
18
|
+
export declare function shellTokensFromValues(values: readonly string[], options?: {
|
|
19
|
+
detectExpansion?: boolean;
|
|
20
|
+
}): ShellToken[];
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
const SHELL_INTERPRETERS = new Set(['bash', 'sh', 'zsh', 'dash', 'fish']);
|
|
3
|
+
const PYTHON_INTERPRETERS = new Set(['python', 'python3']);
|
|
4
|
+
const SHELL_SHORT_OPTIONS = new Set(['c', 'l', 'e', 'x', 'u']);
|
|
5
|
+
const SHELL_NON_SCRIPT_SHORT_OPTIONS = new Set(['n']);
|
|
6
|
+
const SHELL_TERMINAL_OPTIONS = new Map([
|
|
7
|
+
['bash', new Set(['--help', '--version'])],
|
|
8
|
+
['zsh', new Set(['--version'])],
|
|
9
|
+
['fish', new Set(['-h', '--help', '-v', '--version'])],
|
|
10
|
+
]);
|
|
11
|
+
const SHELL_VALUE_OPTIONS = new Set(['-O', '+O', '--init-file', '--rcfile']);
|
|
12
|
+
const NODE_TERMINAL_OPTIONS = new Set(['-h', '--help', '--help-all', '-v', '--version']);
|
|
13
|
+
const NODE_FILE_OPTIONS = new Set(['-c', '--check']);
|
|
14
|
+
const PYTHON_PROFILE = {
|
|
15
|
+
scriptOptions: new Set(['-c']),
|
|
16
|
+
terminalOptions: new Set(['-h', '--help', '-V', '-VV', '--version']),
|
|
17
|
+
terminalValueOptions: new Set(['-m']),
|
|
18
|
+
flagOptions: new Set([
|
|
19
|
+
'-b',
|
|
20
|
+
'-bb',
|
|
21
|
+
'-B',
|
|
22
|
+
'-d',
|
|
23
|
+
'-E',
|
|
24
|
+
'-I',
|
|
25
|
+
'-O',
|
|
26
|
+
'-OO',
|
|
27
|
+
'-P',
|
|
28
|
+
'-q',
|
|
29
|
+
'-s',
|
|
30
|
+
'-S',
|
|
31
|
+
'-u',
|
|
32
|
+
'-v',
|
|
33
|
+
'-x',
|
|
34
|
+
]),
|
|
35
|
+
valueOptions: new Set(['-W', '-X']),
|
|
36
|
+
attachedValuePrefixes: ['-W', '-X'],
|
|
37
|
+
};
|
|
38
|
+
const RUBY_PROFILE = {
|
|
39
|
+
scriptOptions: new Set(['-e']),
|
|
40
|
+
terminalOptions: new Set(['-h', '--help', '-v', '--version', '--copyright']),
|
|
41
|
+
terminalValueOptions: new Set([]),
|
|
42
|
+
flagOptions: new Set(['-d', '--debug', '-w']),
|
|
43
|
+
valueOptions: new Set(['-I']),
|
|
44
|
+
attachedValuePrefixes: ['-I'],
|
|
45
|
+
};
|
|
46
|
+
const PERL_PROFILE = {
|
|
47
|
+
scriptOptions: new Set(['-e']),
|
|
48
|
+
terminalOptions: new Set(['-h', '--help', '-v', '--version']),
|
|
49
|
+
terminalValueOptions: new Set([]),
|
|
50
|
+
flagOptions: new Set([]),
|
|
51
|
+
valueOptions: new Set(['-I']),
|
|
52
|
+
attachedValuePrefixes: ['-I'],
|
|
53
|
+
};
|
|
54
|
+
const OSASCRIPT_PROFILE = {
|
|
55
|
+
scriptOptions: new Set(['-e']),
|
|
56
|
+
terminalOptions: new Set(['-h', '--help']),
|
|
57
|
+
terminalValueOptions: new Set([]),
|
|
58
|
+
flagOptions: new Set([]),
|
|
59
|
+
valueOptions: new Set(['-l']),
|
|
60
|
+
attachedValuePrefixes: [],
|
|
61
|
+
};
|
|
62
|
+
function normalizeInterpreter(value) {
|
|
63
|
+
return path.basename(value);
|
|
64
|
+
}
|
|
65
|
+
function scriptResult(interpreter, token) {
|
|
66
|
+
if (!token) {
|
|
67
|
+
return { kind: 'indeterminate', interpreter, signal: 'shell.interpreter_argv_incomplete' };
|
|
68
|
+
}
|
|
69
|
+
if (token.parts.some((part) => part.hasExpansion)) {
|
|
70
|
+
return { kind: 'dynamic', interpreter, signal: 'shell.script_expanded' };
|
|
71
|
+
}
|
|
72
|
+
return { kind: 'static', interpreter, script: token.value };
|
|
73
|
+
}
|
|
74
|
+
function decodeShell(words, interpreter) {
|
|
75
|
+
for (let index = 1; index < words.length; index += 1) {
|
|
76
|
+
const option = words[index]?.value ?? '';
|
|
77
|
+
if (option === '--')
|
|
78
|
+
return { kind: 'none' };
|
|
79
|
+
if (SHELL_TERMINAL_OPTIONS.get(interpreter)?.has(option))
|
|
80
|
+
return { kind: 'none' };
|
|
81
|
+
if (interpreter === 'bash' && SHELL_VALUE_OPTIONS.has(option)) {
|
|
82
|
+
const operand = words[index + 1]?.value;
|
|
83
|
+
if (!operand || operand.startsWith('-')) {
|
|
84
|
+
return { kind: 'indeterminate', interpreter, signal: 'shell.interpreter_option_unknown' };
|
|
85
|
+
}
|
|
86
|
+
index += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!option.startsWith('-') || option === '-')
|
|
90
|
+
return { kind: 'none' };
|
|
91
|
+
const flags = [...option.slice(1)];
|
|
92
|
+
if (flags.length === 0) {
|
|
93
|
+
return { kind: 'indeterminate', interpreter, signal: 'shell.interpreter_option_unknown' };
|
|
94
|
+
}
|
|
95
|
+
if (flags.every((flag) => SHELL_SHORT_OPTIONS.has(flag))) {
|
|
96
|
+
if (!flags.includes('c'))
|
|
97
|
+
continue;
|
|
98
|
+
return scriptResult(interpreter, words[index + 1]);
|
|
99
|
+
}
|
|
100
|
+
if (flags.every((flag) => SHELL_SHORT_OPTIONS.has(flag) || SHELL_NON_SCRIPT_SHORT_OPTIONS.has(flag)) &&
|
|
101
|
+
flags.some((flag) => SHELL_NON_SCRIPT_SHORT_OPTIONS.has(flag))) {
|
|
102
|
+
return { kind: 'none' };
|
|
103
|
+
}
|
|
104
|
+
return { kind: 'indeterminate', interpreter, signal: 'shell.interpreter_option_unknown' };
|
|
105
|
+
}
|
|
106
|
+
return { kind: 'none' };
|
|
107
|
+
}
|
|
108
|
+
function decodeSeparated(words, interpreter, profile) {
|
|
109
|
+
for (let index = 1; index < words.length; index += 1) {
|
|
110
|
+
const option = words[index]?.value ?? '';
|
|
111
|
+
if (option === '--' || !option.startsWith('-') || option === '-')
|
|
112
|
+
return { kind: 'none' };
|
|
113
|
+
if (profile.scriptOptions.has(option)) {
|
|
114
|
+
return scriptResult(interpreter, words[index + 1]);
|
|
115
|
+
}
|
|
116
|
+
if (profile.terminalOptions.has(option))
|
|
117
|
+
return { kind: 'none' };
|
|
118
|
+
if (profile.terminalValueOptions.has(option)) {
|
|
119
|
+
const operand = words[index + 1]?.value;
|
|
120
|
+
if (!operand || operand.startsWith('-')) {
|
|
121
|
+
return { kind: 'indeterminate', interpreter, signal: 'shell.interpreter_option_unknown' };
|
|
122
|
+
}
|
|
123
|
+
return { kind: 'none' };
|
|
124
|
+
}
|
|
125
|
+
if (profile.flagOptions.has(option)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (profile.valueOptions.has(option)) {
|
|
129
|
+
const operand = words[index + 1]?.value;
|
|
130
|
+
if (!operand || operand.startsWith('-')) {
|
|
131
|
+
return { kind: 'indeterminate', interpreter, signal: 'shell.interpreter_option_unknown' };
|
|
132
|
+
}
|
|
133
|
+
index += 1;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (profile.attachedValuePrefixes.some((prefix) => option.startsWith(prefix) && option.length > prefix.length)) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
return { kind: 'indeterminate', interpreter, signal: 'shell.interpreter_option_unknown' };
|
|
140
|
+
}
|
|
141
|
+
return { kind: 'none' };
|
|
142
|
+
}
|
|
143
|
+
function decodeNode(words, interpreter) {
|
|
144
|
+
const option = words[1]?.value ?? '';
|
|
145
|
+
if (option === '--' || !option.startsWith('-') || option === '-')
|
|
146
|
+
return { kind: 'none' };
|
|
147
|
+
if (option === '-e' || option === '--eval') {
|
|
148
|
+
return scriptResult(interpreter, words[2]);
|
|
149
|
+
}
|
|
150
|
+
if (option.startsWith('--eval=')) {
|
|
151
|
+
const script = option.slice('--eval='.length);
|
|
152
|
+
if (words[1]?.parts.some((part) => part.hasExpansion)) {
|
|
153
|
+
return { kind: 'dynamic', interpreter, signal: 'shell.script_expanded' };
|
|
154
|
+
}
|
|
155
|
+
return { kind: 'static', interpreter, script };
|
|
156
|
+
}
|
|
157
|
+
if (NODE_TERMINAL_OPTIONS.has(option))
|
|
158
|
+
return { kind: 'none' };
|
|
159
|
+
if (NODE_FILE_OPTIONS.has(option))
|
|
160
|
+
return { kind: 'none' };
|
|
161
|
+
return { kind: 'indeterminate', interpreter, signal: 'shell.interpreter_option_unknown' };
|
|
162
|
+
}
|
|
163
|
+
function decodeEval(words) {
|
|
164
|
+
const arguments_ = words.slice(1);
|
|
165
|
+
if (arguments_.length === 0)
|
|
166
|
+
return { kind: 'none' };
|
|
167
|
+
if (arguments_.some((word) => word.parts.some((part) => part.hasExpansion))) {
|
|
168
|
+
return { kind: 'dynamic', interpreter: 'eval', signal: 'shell.script_expanded' };
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
kind: 'static',
|
|
172
|
+
interpreter: 'eval',
|
|
173
|
+
script: arguments_.map((word) => word.value).join(' '),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export function decodeRecursiveInvocation(tokens) {
|
|
177
|
+
if (tokens.some((token) => token.kind === 'operator'))
|
|
178
|
+
return { kind: 'none' };
|
|
179
|
+
const words = tokens.filter((token) => token.kind === 'word');
|
|
180
|
+
const interpreter = normalizeInterpreter(words[0]?.value ?? '');
|
|
181
|
+
if (!interpreter)
|
|
182
|
+
return { kind: 'none' };
|
|
183
|
+
if (interpreter === 'eval')
|
|
184
|
+
return decodeEval(words);
|
|
185
|
+
if (SHELL_INTERPRETERS.has(interpreter))
|
|
186
|
+
return decodeShell(words, interpreter);
|
|
187
|
+
if (PYTHON_INTERPRETERS.has(interpreter)) {
|
|
188
|
+
return decodeSeparated(words, interpreter, PYTHON_PROFILE);
|
|
189
|
+
}
|
|
190
|
+
if (interpreter === 'node')
|
|
191
|
+
return decodeNode(words, interpreter);
|
|
192
|
+
if (interpreter === 'ruby')
|
|
193
|
+
return decodeSeparated(words, interpreter, RUBY_PROFILE);
|
|
194
|
+
if (interpreter === 'perl')
|
|
195
|
+
return decodeSeparated(words, interpreter, PERL_PROFILE);
|
|
196
|
+
if (interpreter === 'osascript')
|
|
197
|
+
return decodeSeparated(words, interpreter, OSASCRIPT_PROFILE);
|
|
198
|
+
return { kind: 'none' };
|
|
199
|
+
}
|
|
200
|
+
export function shellTokensFromValues(values, options = {}) {
|
|
201
|
+
let offset = 0;
|
|
202
|
+
return values.map((value) => {
|
|
203
|
+
const start = offset;
|
|
204
|
+
const end = start + value.length;
|
|
205
|
+
offset = end + 1;
|
|
206
|
+
return {
|
|
207
|
+
kind: 'word',
|
|
208
|
+
value,
|
|
209
|
+
raw: value,
|
|
210
|
+
start,
|
|
211
|
+
end,
|
|
212
|
+
parts: [
|
|
213
|
+
{
|
|
214
|
+
value,
|
|
215
|
+
raw: value,
|
|
216
|
+
start,
|
|
217
|
+
end,
|
|
218
|
+
quote: 'unquoted',
|
|
219
|
+
hasExpansion: options.detectExpansion !== false && (value.includes('$') || value.includes('`')),
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
};
|
|
223
|
+
});
|
|
224
|
+
}
|
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* These commands guard classifier availability in tests and never grant runtime authority.
|
|
4
4
|
* @see src/__tests__/verdict/structural-suite.test.ts
|
|
5
5
|
*/
|
|
6
|
-
export declare const BENIGN_PROBE_CORES: readonly ["npm test", "npm run build", "pnpm test", "pnpm build", "pnpm vitest run src/example.test.ts", "bash -lc 'git status'", "belay approve belay_deadbeef1234", "bundle -v", "ruby -v", "yarn --version", "make -n test", "bin/rails routes", "bundle exec rubocop --version"];
|
|
6
|
+
export declare const BENIGN_PROBE_CORES: readonly ["npm test", "npm run build", "pnpm test", "pnpm build", "pnpm vitest run src/example.test.ts", "bash -lc 'git status'", "belay approve belay_deadbeef1234", "bundle -v", "ruby -v", "yarn --version", "make -n test", "bin/rails routes", "bundle exec rubocop --version", "bundle exec rubocop test/upgrade_script_contract_test.rb", "ruby -Itest test/upgrade_script_contract_test.rb"];
|
package/dist/defaults.js
CHANGED
|
@@ -59,6 +59,15 @@ export function getManagedHookEntries(platform = process.platform, hooksDir, rep
|
|
|
59
59
|
matcher: 'Delete',
|
|
60
60
|
},
|
|
61
61
|
},
|
|
62
|
+
{
|
|
63
|
+
event: 'preToolUse',
|
|
64
|
+
definition: {
|
|
65
|
+
command: toolGate,
|
|
66
|
+
placement: 'prepend',
|
|
67
|
+
// Agent Shell tool (preToolUse) — distinct from terminal beforeShellExecution.
|
|
68
|
+
matcher: 'Shell',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
62
71
|
{
|
|
63
72
|
event: 'subagentStart',
|
|
64
73
|
definition: {
|
|
@@ -114,6 +123,13 @@ export function getManagedHookEntries(platform = process.platform, hooksDir, rep
|
|
|
114
123
|
placement: 'append',
|
|
115
124
|
},
|
|
116
125
|
},
|
|
126
|
+
{
|
|
127
|
+
event: 'postToolUseFailure',
|
|
128
|
+
definition: {
|
|
129
|
+
command: runnerCommand(platform, resolvedHooksDir, resolvedRepo, 'belay-audit', 'postToolUseFailure'),
|
|
130
|
+
placement: 'append',
|
|
131
|
+
},
|
|
132
|
+
},
|
|
117
133
|
{
|
|
118
134
|
event: 'stop',
|
|
119
135
|
definition: {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type ScopedPaths } from '../adapters/layouts/scope.js';
|
|
2
2
|
import type { AdapterName } from '../adapters/layouts/types.js';
|
|
3
3
|
import type { BelayConfigV4 } from '../core/config.js';
|
|
4
|
-
import type { InitOptions, UpgradeOptions } from '../types.js';
|
|
4
|
+
import type { InitOptions, UninstallOptions, UpgradeOptions } from '../types.js';
|
|
5
5
|
export type OperationScope = 'project' | 'global';
|
|
6
|
-
export declare function resolveOperationScope(repoRoot: string, adapter: AdapterName, options?: InitOptions | UpgradeOptions): Promise<OperationScope>;
|
|
6
|
+
export declare function resolveOperationScope(repoRoot: string, adapter: AdapterName, options?: InitOptions | UpgradeOptions | UninstallOptions): Promise<OperationScope>;
|
|
7
7
|
export declare function applyInstallScope(repoRoot: string, adapter: AdapterName, scope: OperationScope, config?: BelayConfigV4): Promise<BelayConfigV4>;
|
|
8
8
|
export declare function pathsForOperation(adapter: AdapterName, scope: OperationScope, repoRoot: string): ScopedPaths;
|
package/dist/installer.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AdapterName } from './adapters/layouts/types.js';
|
|
2
|
-
import type { HooksFile, InitOptions, UpgradeOptions } from './types.js';
|
|
2
|
+
import type { HooksFile, InitOptions, UninstallOptions, UpgradeOptions } from './types.js';
|
|
3
3
|
export type { InstallScope } from './adapters/layouts/scope.js';
|
|
4
4
|
export declare function loadHooksFile(hooksPath: string): Promise<HooksFile>;
|
|
5
5
|
export declare function mergeHooksFile(current: HooksFile, platform?: NodeJS.Platform, hooksDir?: string, repoRoot?: string): HooksFile;
|
|
@@ -10,6 +10,10 @@ export declare function initCursorProject(options?: InitOptions): Promise<{
|
|
|
10
10
|
export declare function upgradeCursorProject(options?: UpgradeOptions): Promise<{
|
|
11
11
|
repoRoot: string;
|
|
12
12
|
}>;
|
|
13
|
+
export declare function uninstallCursorProject(options?: UninstallOptions): Promise<{
|
|
14
|
+
repoRoot: string;
|
|
15
|
+
scope: 'project' | 'global';
|
|
16
|
+
}>;
|
|
13
17
|
export declare function initProject(options?: InitOptions): Promise<{
|
|
14
18
|
repoRoot: string;
|
|
15
19
|
withSkill: boolean;
|
|
@@ -20,3 +24,8 @@ export declare function upgradeProject(options?: UpgradeOptions): Promise<{
|
|
|
20
24
|
repoRoot: string;
|
|
21
25
|
adapter: AdapterName;
|
|
22
26
|
}>;
|
|
27
|
+
export declare function uninstallProject(options?: UninstallOptions): Promise<{
|
|
28
|
+
repoRoot: string;
|
|
29
|
+
adapter: AdapterName;
|
|
30
|
+
scope: 'project' | 'global';
|
|
31
|
+
}>;
|
package/dist/installer.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { mergeCursorHooksFile } from './adapters/cursor/hooks.js';
|
|
4
|
+
import { mergeCursorHooksFile, stripCursorHooksFile } from './adapters/cursor/hooks.js';
|
|
5
5
|
import { cursorLayout } from './adapters/layouts/cursor.js';
|
|
6
6
|
import { resolveScopedPaths } from './adapters/layouts/scope.js';
|
|
7
7
|
import { getAdapter } from './adapters/registry.js';
|
|
@@ -94,6 +94,38 @@ export async function upgradeCursorProject(options = {}) {
|
|
|
94
94
|
await archiveLegacyAuditLogIfNeeded(repoRoot, config);
|
|
95
95
|
return { repoRoot };
|
|
96
96
|
}
|
|
97
|
+
const BELAY_HOOK_ARTIFACTS = [
|
|
98
|
+
'belay-before-submit.mjs',
|
|
99
|
+
'belay-shell-gate.mjs',
|
|
100
|
+
'belay-tool-gate.mjs',
|
|
101
|
+
'belay-audit.mjs',
|
|
102
|
+
'belay-runner',
|
|
103
|
+
'belay-runner.cmd',
|
|
104
|
+
];
|
|
105
|
+
async function removeBelayHookArtifacts(paths) {
|
|
106
|
+
for (const fileName of BELAY_HOOK_ARTIFACTS) {
|
|
107
|
+
await rm(path.join(paths.hooksDir, fileName), { force: true });
|
|
108
|
+
}
|
|
109
|
+
await rm(paths.runtimeDir, { recursive: true, force: true });
|
|
110
|
+
await rm(paths.skillsDir, { recursive: true, force: true });
|
|
111
|
+
if (paths.commandsDir) {
|
|
112
|
+
await rm(path.join(paths.commandsDir, 'belay.md'), { force: true });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export async function uninstallCursorProject(options = {}) {
|
|
116
|
+
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
117
|
+
const scope = await resolveOperationScope(repoRoot, 'cursor', options);
|
|
118
|
+
const paths = resolveScopedPaths(cursorLayout, scope, repoRoot);
|
|
119
|
+
const hooksSettingsExisted = existsSync(paths.hooksSettingsPath);
|
|
120
|
+
const hooksFile = await loadHooksFile(paths.hooksSettingsPath);
|
|
121
|
+
const stripped = stripCursorHooksFile(hooksFile, process.platform, paths.hooksDir, repoRoot);
|
|
122
|
+
if (hooksSettingsExisted) {
|
|
123
|
+
await mkdir(path.dirname(paths.hooksSettingsPath), { recursive: true });
|
|
124
|
+
await writeFile(paths.hooksSettingsPath, `${JSON.stringify(stripped, null, 2)}\n`, 'utf8');
|
|
125
|
+
}
|
|
126
|
+
await removeBelayHookArtifacts(paths);
|
|
127
|
+
return { repoRoot, scope };
|
|
128
|
+
}
|
|
97
129
|
function resolveAdapterName(options, repoRoot) {
|
|
98
130
|
if (options.adapter === 'claude') {
|
|
99
131
|
return 'claude';
|
|
@@ -225,3 +257,12 @@ export async function upgradeProject(options = {}) {
|
|
|
225
257
|
await refreshIntegrityManifest(repoRoot, adapterName);
|
|
226
258
|
return { repoRoot, adapter: adapterName };
|
|
227
259
|
}
|
|
260
|
+
export async function uninstallProject(options = {}) {
|
|
261
|
+
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
262
|
+
const adapterName = resolveAdapterName(options, repoRoot);
|
|
263
|
+
if (adapterName !== 'cursor') {
|
|
264
|
+
throw new Error(`belay uninstall is only supported for Cursor adapter today. Use --adapter cursor or uninstall hooks manually for ${adapterName}.`);
|
|
265
|
+
}
|
|
266
|
+
const result = await uninstallCursorProject({ ...options, targetDir: repoRoot });
|
|
267
|
+
return { repoRoot: result.repoRoot, adapter: adapterName, scope: result.scope };
|
|
268
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type { BelayConfig, BelayConfigV1, BelayConfigV2, BelayConfigV3, BelayControlPlaneConfig, BelayEgressConfig, BelayOverridesConfig, BelayPolicyConfig, BelayRedactionConfig, BelaySandboxConfig, BelayTransactionalConfig, UnknownLocalEffectPolicy, } from './core/config.js';
|
|
2
2
|
export type { ApprovalRecord, ApprovalStateFile, Assessment, BelayMode, ClassifyResult, HookVerdict, } from './core/types.js';
|
|
3
3
|
import type { InstallScope } from './adapters/layouts/scope.js';
|
|
4
|
-
import type { RecentAskEntry } from './core/audit-summary.js';
|
|
4
|
+
import type { RecentAskEntry, RecentHostDenialEntry } from './core/audit-summary.js';
|
|
5
5
|
import type { BelayEgressConfig, BelayOverridesConfig, BelayPolicyConfig, BelaySandboxConfig } from './core/config.js';
|
|
6
6
|
import type { ApprovalRecord, ClassifyResult } from './core/types.js';
|
|
7
7
|
export interface HookEntry {
|
|
@@ -42,6 +42,32 @@ export interface UpgradeOptions {
|
|
|
42
42
|
/** Opt-in: migrate implicit factory-default ollama judge to host default provider. */
|
|
43
43
|
migrateJudgeDefault?: boolean;
|
|
44
44
|
}
|
|
45
|
+
export interface UninstallOptions {
|
|
46
|
+
targetDir?: string;
|
|
47
|
+
adapter?: AdapterName;
|
|
48
|
+
scope?: InstallScope;
|
|
49
|
+
}
|
|
50
|
+
export interface WhereOptions {
|
|
51
|
+
targetDir?: string;
|
|
52
|
+
adapter?: AdapterName;
|
|
53
|
+
scope?: InstallScope;
|
|
54
|
+
json?: boolean;
|
|
55
|
+
}
|
|
56
|
+
export interface WhereReport {
|
|
57
|
+
cwd: string;
|
|
58
|
+
repoRoot: string;
|
|
59
|
+
adapter: AdapterName;
|
|
60
|
+
installScope: 'project' | 'global';
|
|
61
|
+
configPresent: boolean;
|
|
62
|
+
cliExecutable?: string;
|
|
63
|
+
cliPackageRoot: string;
|
|
64
|
+
configPath: string;
|
|
65
|
+
hooksSettingsPath: string;
|
|
66
|
+
hooksDir: string;
|
|
67
|
+
runtimeDir: string;
|
|
68
|
+
skillsDir: string;
|
|
69
|
+
commandsDir?: string;
|
|
70
|
+
}
|
|
45
71
|
export interface DoctorOptions {
|
|
46
72
|
targetDir?: string;
|
|
47
73
|
fix?: boolean;
|
|
@@ -88,6 +114,10 @@ export interface StatusOptions {
|
|
|
88
114
|
export interface HealthSnapshotOptions {
|
|
89
115
|
targetDir?: string;
|
|
90
116
|
adapter?: AdapterName;
|
|
117
|
+
/** Override the host home directory when inspecting adapter-level execution settings. */
|
|
118
|
+
homeDir?: string;
|
|
119
|
+
/** Override Cursor config-path environment variables for deterministic host inspection. */
|
|
120
|
+
cursorConfigEnv?: Partial<Pick<NodeJS.ProcessEnv, 'CURSOR_CONFIG_DIR' | 'XDG_CONFIG_HOME'>>;
|
|
91
121
|
}
|
|
92
122
|
export interface HealthSnapshot {
|
|
93
123
|
repoRoot: string;
|
|
@@ -172,6 +202,9 @@ export interface AuditVisibilityReport {
|
|
|
172
202
|
allowCount: number;
|
|
173
203
|
silentPassRate: number;
|
|
174
204
|
recentAsks: RecentAskEntry[];
|
|
205
|
+
hostDeniedAfterAllowCount?: number;
|
|
206
|
+
recentHostDenials?: RecentHostDenialEntry[];
|
|
207
|
+
unrecognizedHostFailureCount?: number;
|
|
175
208
|
warnings: string[];
|
|
176
209
|
notes: string[];
|
|
177
210
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const PACKAGE_VERSION = "0.9.
|
|
1
|
+
export declare const PACKAGE_VERSION = "0.9.3";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/sync-version.mjs — do not edit.
|
|
2
|
-
export const PACKAGE_VERSION = '0.9.
|
|
2
|
+
export const PACKAGE_VERSION = '0.9.3';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guilz-dev/belay",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"description": "Belay-style approval and audit gating for agent runtimes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -57,10 +57,13 @@
|
|
|
57
57
|
"typecheck": "tsc --noEmit",
|
|
58
58
|
"test": "pnpm build && vitest run",
|
|
59
59
|
"test:structural": "pnpm build && vitest run src/__tests__/verdict/structural-suite.test.ts",
|
|
60
|
-
"test:docker": "pnpm build && vitest run src/__tests__/capability/boundary-container-isolation.test.ts src/__tests__/capability/boundary-driver-container.test.ts",
|
|
60
|
+
"test:docker": "pnpm build && vitest run --config vitest.live.config.ts src/__tests__/capability/boundary-container-isolation.test.ts src/__tests__/capability/boundary-container-workspace-mount.test.ts src/__tests__/capability/boundary-driver-container.test.ts src/__tests__/contained-execution-docker.integration.test.ts",
|
|
61
|
+
"test:llm": "pnpm build && vitest run --config vitest.live.config.ts src/__tests__/verdict/llm/judge-accuracy.test.ts",
|
|
61
62
|
"test:stable": "pnpm build && vitest run && vitest run && vitest run",
|
|
62
63
|
"corpus": "pnpm build && node scripts/corpus.mjs",
|
|
63
64
|
"probe:adversarial": "pnpm build && node scripts/adversarial-probe.mjs",
|
|
65
|
+
"probe:cursor-shell-rewrite": "node scripts/cursor-shell-rewrite-probe.mjs",
|
|
66
|
+
"probe:native-seatbelt-boundary": "node scripts/native-seatbelt-boundary-probe.mjs --live",
|
|
64
67
|
"probe:coverage": "pnpm build && node scripts/coverage-probe.mjs",
|
|
65
68
|
"probe:coverage:repeat": "pnpm build && node scripts/coverage-probe.mjs --repeat 3",
|
|
66
69
|
"corpus:ratchet": "pnpm build && node scripts/corpus-ratchet.mjs"
|
package/skills/belay/SKILL.md
CHANGED
|
@@ -54,6 +54,11 @@ To restore the legacy two-step UX (approve, then always retry manually), set
|
|
|
54
54
|
For why it was blocked, use `/belay why <command>` or `belay explain --command "<command>"`.
|
|
55
55
|
For the latest pending ask, use `/belay explain` or `belay explain`.
|
|
56
56
|
|
|
57
|
+
If `belay status` or `belay report` shows `Host denied after Belay allow`, the denial came from
|
|
58
|
+
the editor/agent host after Belay returned allow. There is no Belay approval ID to approve. Keep
|
|
59
|
+
host protection in Belay audit mode and approve only the exact host prompt when execution is
|
|
60
|
+
intended.
|
|
61
|
+
|
|
57
62
|
**Do not use command allowlists** (`overrides.allow`) or legacy standing-allow records
|
|
58
63
|
(shell, tool, or subagent) to fix blocks; none of them change runtime authorization.
|
|
59
64
|
Improve EffectPlan semantics, approve once with `/belay-approve`, or use an exact
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
Show audit visibility: ask/flag/allow counts, silent-pass rate,
|
|
1
|
+
Show audit visibility: ask/flag/allow counts, silent-pass rate, recent asks, and host
|
|
2
|
+
`permission_denied` failures correlated to a preceding Belay allow.
|
|
2
3
|
|
|
3
4
|
```bash
|
|
4
5
|
belay report
|
|
5
6
|
```
|
|
6
7
|
|
|
7
8
|
For full install health plus audit summary, use `belay status`.
|
|
9
|
+
|
|
10
|
+
`Host denied after Belay allow` is host-policy telemetry, not a pending Belay approval.
|