@hasna/hooks 0.11.7 → 0.12.1
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 +129 -4
- package/bin/hooks-mcp.js +39 -7
- package/bin/index.js +739 -129
- package/bin/native-safety-entry.js +15 -1718
- package/bin/serve.js +36 -9
- package/dist/index.js +51 -17
- package/dist/lib/codex-safety-check.d.ts +3 -0
- package/dist/lib/native-safety-registration.d.ts +49 -6
- package/dist/lib/registry.d.ts +15 -0
- package/dist/native-safety.d.ts +4 -0
- package/dist/native-safety.js +8 -4
- package/dist/sdk/index.js +1 -1
- package/hooks/hook-signed-link-guard/README.md +228 -0
- package/hooks/hook-signed-link-guard/package.json +12 -0
- package/hooks/hook-signed-link-guard/src/classify.ts +1280 -0
- package/hooks/hook-signed-link-guard/src/hook.ts +72 -0
- package/hooks/hook-signed-link-guard/src/jq.ts +545 -0
- package/hooks/hook-signed-link-guard/src/links.ts +136 -0
- package/hooks/hook-signed-link-output/README.md +47 -0
- package/hooks/hook-signed-link-output/package.json +12 -0
- package/hooks/hook-signed-link-output/src/hook.ts +111 -0
- package/hooks/hook-trash-guard/src/hook.ts +204 -80
- package/hooks/native-safety-entry.ts +28 -7
- package/package.json +2 -2
- package/scripts/validate-package.ts +12 -2
|
@@ -1,1719 +1,16 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import { dirname, isAbsolute, join, resolve } from "path";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
unsafe_directory: "A Trash PATH or package directory is not owned safely or is writable by group or others.",
|
|
18
|
-
unsafe_interpreter: "The Trash interpreter is missing, unsupported, or not a safe owned executable on its actual PATH.",
|
|
19
|
-
unrecognized_package: "The candidate is not the registered @hasna/trash executable.",
|
|
20
|
-
identity_failed: "The installed Trash identity probe failed or exceeded its deadline.",
|
|
21
|
-
unsupported_protocol: "The installed Trash version does not expose the required guard protocol."
|
|
22
|
-
};
|
|
23
|
-
function trustedOwner(uid) {
|
|
24
|
-
return uid === 0 || uid === process.getuid?.();
|
|
25
|
-
}
|
|
26
|
-
function safeExecutableFile(path, systemBinAlias = false) {
|
|
27
|
-
let current = path;
|
|
28
|
-
for (let hop = 0;hop < 32; hop++) {
|
|
29
|
-
if (!isAbsolute(current) || current.length > 4096 || !safeBinaryDirectory("/"))
|
|
30
|
-
return false;
|
|
31
|
-
const parts = current.split("/"), name = parts.pop();
|
|
32
|
-
if (!name || name === "." || name === "..")
|
|
33
|
-
return false;
|
|
34
|
-
let parent = "/";
|
|
35
|
-
for (const part of parts) {
|
|
36
|
-
if (!part || part === ".")
|
|
37
|
-
continue;
|
|
38
|
-
if (part === "..") {
|
|
39
|
-
parent = dirname(parent);
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
const next = join(parent, part), info2 = lstatSync(next);
|
|
43
|
-
if (systemBinAlias && next === "/bin" && info2.isSymbolicLink() && info2.uid === 0) {
|
|
44
|
-
const target = realpathSync(next);
|
|
45
|
-
if (!safeBinaryDirectory(target))
|
|
46
|
-
return false;
|
|
47
|
-
parent = target;
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (!info2.isDirectory() || !trustedOwner(info2.uid) || info2.mode & 18)
|
|
51
|
-
return false;
|
|
52
|
-
parent = next;
|
|
53
|
-
}
|
|
54
|
-
const candidate = join(parent, name), info = lstatSync(candidate);
|
|
55
|
-
if (!trustedOwner(info.uid))
|
|
56
|
-
return false;
|
|
57
|
-
if (info.isSymbolicLink()) {
|
|
58
|
-
const target = readlinkSync(candidate);
|
|
59
|
-
current = isAbsolute(target) ? target : parent + "/" + target;
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
return info.isFile() && Boolean(info.mode & 73) && !(info.mode & 18);
|
|
63
|
-
}
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
function safeInterpreterSearchDirectory(path) {
|
|
67
|
-
if (!isAbsolute(path) || resolve(path) !== (path.replace(/\/+$/, "") || "/"))
|
|
68
|
-
return false;
|
|
69
|
-
for (let cursor = resolve(path);; cursor = dirname(cursor)) {
|
|
70
|
-
try {
|
|
71
|
-
lstatSync(cursor);
|
|
72
|
-
return safeBinaryDirectory(cursor);
|
|
73
|
-
} catch (error) {
|
|
74
|
-
if (error.code !== "ENOENT")
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
if (dirname(cursor) === cursor)
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
function safeScriptInterpreter(executable, pathValue) {
|
|
82
|
-
const fd = openSync(executable, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
|
|
83
|
-
let header;
|
|
84
|
-
try {
|
|
85
|
-
if (!fstatSync(fd).isFile())
|
|
86
|
-
return false;
|
|
87
|
-
const bytes = Buffer.alloc(256), length = readSync(fd, bytes, 0, bytes.length, 0);
|
|
88
|
-
header = bytes.subarray(0, length).toString("utf8").split(`
|
|
89
|
-
`)[0];
|
|
90
|
-
} finally {
|
|
91
|
-
closeSync(fd);
|
|
92
|
-
}
|
|
93
|
-
const direct = /^#!(\/[A-Za-z0-9_./-]+)$/.exec(header);
|
|
94
|
-
if (direct && direct[1] !== "/usr/bin/env")
|
|
95
|
-
return safeExecutableFile(direct[1], true);
|
|
96
|
-
if (header !== "#!/usr/bin/env bun" || !safeExecutableFile("/usr/bin/env", true))
|
|
97
|
-
return false;
|
|
98
|
-
for (const dir of pathValue.split(":").slice(0, 64)) {
|
|
99
|
-
if (!safeInterpreterSearchDirectory(dir))
|
|
100
|
-
return false;
|
|
101
|
-
const interpreter = resolve(dir, "bun");
|
|
102
|
-
try {
|
|
103
|
-
if (!statSync(interpreter).isFile())
|
|
104
|
-
continue;
|
|
105
|
-
accessSync(interpreter, constants.X_OK);
|
|
106
|
-
} catch {
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
return safeExecutableFile(interpreter, true);
|
|
110
|
-
}
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
function safeBinaryDirectory(path) {
|
|
114
|
-
let current = path;
|
|
115
|
-
for (;; ) {
|
|
116
|
-
const info = lstatSync(current);
|
|
117
|
-
if (!info.isDirectory() || !trustedOwner(info.uid) || (info.mode & 18) !== 0)
|
|
118
|
-
return false;
|
|
119
|
-
const parent = dirname(current);
|
|
120
|
-
if (parent === current)
|
|
121
|
-
return true;
|
|
122
|
-
current = parent;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
function inspectTrashBinary(env = process.env) {
|
|
126
|
-
const pathValue = env.PATH ?? "";
|
|
127
|
-
const deadline = Date.now() + 2000;
|
|
128
|
-
let failure = "missing";
|
|
129
|
-
const reject = (code) => {
|
|
130
|
-
if (failure === "missing" || failure === "unrecognized_package" || code.startsWith("unsafe_"))
|
|
131
|
-
failure = code;
|
|
132
|
-
};
|
|
133
|
-
for (const dir of pathValue.split(":").slice(0, 64)) {
|
|
134
|
-
if (!isAbsolute(dir) || Date.now() >= deadline)
|
|
135
|
-
continue;
|
|
136
|
-
const candidate = resolve(dir, "trash");
|
|
137
|
-
try {
|
|
138
|
-
const stat = statSync(candidate);
|
|
139
|
-
if (!stat.isFile() || !trustedOwner(stat.uid) || (stat.mode & 73) === 0 || (stat.mode & 18) !== 0) {
|
|
140
|
-
reject("unsafe_executable");
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
const executable = realpathSync(candidate);
|
|
144
|
-
if (!safeBinaryDirectory(resolve(dir)) || !safeBinaryDirectory(dirname(executable))) {
|
|
145
|
-
reject("unsafe_directory");
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
if (!safeExecutableFile(candidate)) {
|
|
149
|
-
reject("unsafe_directory");
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
const packageRoot = resolve(dirname(executable), "../..");
|
|
153
|
-
const manifestPath = join(packageRoot, "package.json");
|
|
154
|
-
const manifestStat = statSync(manifestPath);
|
|
155
|
-
if (!manifestStat.isFile() || !trustedOwner(manifestStat.uid) || manifestStat.size > 16384 || (manifestStat.mode & 18) !== 0 || lstatSync(manifestPath).isSymbolicLink()) {
|
|
156
|
-
reject("unsafe_manifest");
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
160
|
-
if (manifest.name !== "@hasna/trash" || typeof manifest.version !== "string" || typeof manifest.bin?.trash !== "string" || realpathSync(resolve(packageRoot, manifest.bin.trash)) !== executable) {
|
|
161
|
-
reject("unrecognized_package");
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
try {
|
|
165
|
-
if (!safeScriptInterpreter(executable, pathValue)) {
|
|
166
|
-
reject("unsafe_interpreter");
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
} catch {
|
|
170
|
-
reject("unsafe_interpreter");
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
const remaining = deadline - Date.now();
|
|
174
|
-
if (remaining <= 0) {
|
|
175
|
-
reject("identity_failed");
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
178
|
-
const probe = spawnSync(candidate, ["--identity"], {
|
|
179
|
-
encoding: "utf8",
|
|
180
|
-
timeout: remaining,
|
|
181
|
-
maxBuffer: 2048,
|
|
182
|
-
env: { PATH: pathValue },
|
|
183
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
184
|
-
});
|
|
185
|
-
if (probe.error || probe.status !== 0 || Date.now() >= deadline) {
|
|
186
|
-
reject("identity_failed");
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
const identity = JSON.parse(probe.stdout);
|
|
190
|
-
if (identity.name === "@hasna/trash" && identity.version === manifest.version && identity.guardProtocol === "hasna.trash.guard.v1")
|
|
191
|
-
return { ok: true, path: candidate, version: manifest.version };
|
|
192
|
-
reject("unsupported_protocol");
|
|
193
|
-
} catch {}
|
|
194
|
-
}
|
|
195
|
-
return { ok: false, code: failure };
|
|
196
|
-
}
|
|
197
|
-
function findTrashBinary(env = process.env) {
|
|
198
|
-
const result = inspectTrashBinary(env);
|
|
199
|
-
return result.ok ? result.path : null;
|
|
200
|
-
}
|
|
201
|
-
// hooks/codewith-native-common.ts
|
|
202
|
-
function getCommand(input) {
|
|
203
|
-
const command = input.tool_input?.command;
|
|
204
|
-
return typeof command === "string" ? command : "";
|
|
205
|
-
}
|
|
206
|
-
var segmentCache = new Map;
|
|
207
|
-
var SYSTEM_PROTECTED_ROOTS = [
|
|
208
|
-
"/",
|
|
209
|
-
"/bin",
|
|
210
|
-
"/boot",
|
|
211
|
-
"/dev",
|
|
212
|
-
"/etc",
|
|
213
|
-
"/home",
|
|
214
|
-
"/lib",
|
|
215
|
-
"/lib32",
|
|
216
|
-
"/lib64",
|
|
217
|
-
"/libx32",
|
|
218
|
-
"/opt",
|
|
219
|
-
"/proc",
|
|
220
|
-
"/root",
|
|
221
|
-
"/run",
|
|
222
|
-
"/sbin",
|
|
223
|
-
"/srv",
|
|
224
|
-
"/sys",
|
|
225
|
-
"/usr",
|
|
226
|
-
"/var",
|
|
227
|
-
"/Applications",
|
|
228
|
-
"/Library",
|
|
229
|
-
"/System",
|
|
230
|
-
"/Users",
|
|
231
|
-
"/Volumes",
|
|
232
|
-
"/private"
|
|
233
|
-
];
|
|
234
|
-
var OPAQUE_BUILTINS = new Set(["eval", "source", ".", "trap", "coproc", "exec"]);
|
|
235
|
-
var COMPOUND_KEYWORDS = new Set(["{", "}", "then", "do", "else", "elif", "fi", "done", "!"]);
|
|
236
|
-
var NAME_BINDING_BUILTINS = new Set(["read", "getopts", "mapfile", "readarray"]);
|
|
237
|
-
var VALUE_BINDING_BUILTINS = new Set(["export", "declare", "typeset", "readonly", "local", "let"]);
|
|
238
|
-
var RSYNC_OPTIONS_WITH_VALUE = new Set([
|
|
239
|
-
"-e",
|
|
240
|
-
"--rsh",
|
|
241
|
-
"--exclude",
|
|
242
|
-
"--exclude-from",
|
|
243
|
-
"--include",
|
|
244
|
-
"--include-from",
|
|
245
|
-
"--filter",
|
|
246
|
-
"--files-from",
|
|
247
|
-
"--rsync-path",
|
|
248
|
-
"--out-format",
|
|
249
|
-
"--log-file",
|
|
250
|
-
"--password-file",
|
|
251
|
-
"--backup-dir",
|
|
252
|
-
"--partial-dir",
|
|
253
|
-
"--compare-dest",
|
|
254
|
-
"--copy-dest",
|
|
255
|
-
"--link-dest"
|
|
256
|
-
]);
|
|
257
|
-
var SHELL_INTERPRETERS = new Set(["sh", "bash", "zsh", "dash", "ksh", "ash", "mksh", "busybox"]);
|
|
258
|
-
var USER_SWITCH_COMMANDS = new Set(["su", "runuser"]);
|
|
259
|
-
var SSH_OPTIONS_WITH_VALUE = new Set([
|
|
260
|
-
"-B",
|
|
261
|
-
"-b",
|
|
262
|
-
"-c",
|
|
263
|
-
"-D",
|
|
264
|
-
"-E",
|
|
265
|
-
"-e",
|
|
266
|
-
"-F",
|
|
267
|
-
"-I",
|
|
268
|
-
"-i",
|
|
269
|
-
"-J",
|
|
270
|
-
"-L",
|
|
271
|
-
"-l",
|
|
272
|
-
"-m",
|
|
273
|
-
"-O",
|
|
274
|
-
"-o",
|
|
275
|
-
"-P",
|
|
276
|
-
"-p",
|
|
277
|
-
"-Q",
|
|
278
|
-
"-R",
|
|
279
|
-
"-S",
|
|
280
|
-
"-W",
|
|
281
|
-
"-w"
|
|
282
|
-
]);
|
|
283
|
-
var SHELL_OPTIONS_WITH_VALUE = new Set(["-o", "+o", "--rcfile", "--init-file"]);
|
|
284
|
-
|
|
285
|
-
// hooks/hook-trash-guard/src/hook.ts
|
|
286
|
-
var DEFAULT_TIMEOUT_MS = 600000;
|
|
287
|
-
var DEFAULT_DESCRIPTION = "Delete via trash guard (rm intercepted and made recoverable)";
|
|
288
|
-
var HANDOFF_ROOTS = [".hasna/repos/clones", "workspace/repos"];
|
|
289
|
-
var PROTECTED_HOME_TREES = [".hasna", ".ssh", ".aws"];
|
|
290
|
-
var REFUSED_VERBS = {
|
|
291
|
-
rmdir: "`rmdir` is a delete that trash-guard does not rewrite (it takes rmdir's flags, not rm's). Re-run it as `rm -d -- <path>` for an empty directory, or `rm -r -- <path>` for a non-empty one, and it is redirected into trash.",
|
|
292
|
-
unlink: "`unlink` is a delete that trash-guard does not rewrite. Re-run it as `rm -- <path>` and it is redirected into trash.",
|
|
293
|
-
shred: "`shred` destroys content irrecoverably and is never allowed. Re-run it as `rm -- <path>` and it is redirected into trash."
|
|
294
|
-
};
|
|
295
|
-
var OPAQUE_COMMANDS = new Set(["sh", "bash", "zsh", "dash", "ksh", "ash", "eval", "source", ".", "su", "runuser", "env"]);
|
|
296
|
-
var WRAPPERS = {
|
|
297
|
-
sudo: ["-u", "--user", "-g", "--group", "-p", "--prompt", "-C", "--chdir", "-h", "--host", "-r", "--role", "-t", "--type", "-U", "--other-user"],
|
|
298
|
-
doas: ["-u", "-C"],
|
|
299
|
-
env: ["-u", "--unset", "-C", "--chdir", "-S", "--split-string"],
|
|
300
|
-
nice: ["-n", "--adjustment"],
|
|
301
|
-
ionice: ["-c", "--class", "-n", "--classdata", "-p", "--pid"],
|
|
302
|
-
stdbuf: ["-i", "-o", "-e", "--input", "--output", "--error"],
|
|
303
|
-
time: ["-f", "--format", "-o", "--output"],
|
|
304
|
-
timeout: ["-s", "--signal", "-k", "--kill-after"],
|
|
305
|
-
nohup: [],
|
|
306
|
-
setsid: [],
|
|
307
|
-
command: [],
|
|
308
|
-
builtin: [],
|
|
309
|
-
exec: ["-a"]
|
|
310
|
-
};
|
|
311
|
-
var WRAPPER_POSITIONALS = {
|
|
312
|
-
timeout: 1
|
|
313
|
-
};
|
|
314
|
-
var APPLET_DISPATCHERS = new Set(["busybox", "toybox"]);
|
|
315
|
-
var KEYWORDS = new Set(["!", "{", "}", "then", "do", "else", "elif", "if", "while", "until", "for", "case", "esac", "fi", "done", "select", "function"]);
|
|
316
|
-
var GIT_VALUE_OPTIONS = new Set(["-C", "-c", "--git-dir", "--work-tree", "--namespace", "--exec-path", "--config-env"]);
|
|
317
|
-
var DELETE_VERB_WORD_RE = /(^|[^A-Za-z0-9_.-])(rm|rmdir|unlink|shred)(?![A-Za-z0-9_.-])/;
|
|
318
|
-
function mentionsDeleteVerb(text) {
|
|
319
|
-
return DELETE_VERB_WORD_RE.test(decodeWord(text));
|
|
320
|
-
}
|
|
321
|
-
function envSplitCommandText(words) {
|
|
322
|
-
return words.map((word) => word.replace(/^-[iv0]*S|^--split-string=/, " ")).join(" ");
|
|
323
|
-
}
|
|
324
|
-
var ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
325
|
-
function baseVerb(word) {
|
|
326
|
-
const slash = word.lastIndexOf("/");
|
|
327
|
-
return slash >= 0 ? word.slice(slash + 1) : word;
|
|
328
|
-
}
|
|
329
|
-
function lexCommand(command) {
|
|
330
|
-
const segments = [];
|
|
331
|
-
const substitutions = [];
|
|
332
|
-
let words = [];
|
|
333
|
-
let redirects = new Set;
|
|
334
|
-
let wordStart = -1;
|
|
335
|
-
let pendingRedirect = false;
|
|
336
|
-
let trustworthy = true;
|
|
337
|
-
let i = 0;
|
|
338
|
-
const n = command.length;
|
|
339
|
-
const closeWord = (end) => {
|
|
340
|
-
if (wordStart >= 0) {
|
|
341
|
-
const index = words.length;
|
|
342
|
-
words.push({ text: command.slice(wordStart, end), start: wordStart, end });
|
|
343
|
-
if (pendingRedirect)
|
|
344
|
-
redirects.add(index);
|
|
345
|
-
wordStart = -1;
|
|
346
|
-
pendingRedirect = false;
|
|
347
|
-
}
|
|
348
|
-
};
|
|
349
|
-
const closeSegment = (end) => {
|
|
350
|
-
closeWord(end);
|
|
351
|
-
if (words.length > 0)
|
|
352
|
-
segments.push({ words, redirects });
|
|
353
|
-
words = [];
|
|
354
|
-
redirects = new Set;
|
|
355
|
-
pendingRedirect = false;
|
|
356
|
-
};
|
|
357
|
-
const scanSingle = (from) => {
|
|
358
|
-
const end = command.indexOf("'", from + 1);
|
|
359
|
-
return end === -1 ? -1 : end + 1;
|
|
360
|
-
};
|
|
361
|
-
const scanBacktick = (from) => {
|
|
362
|
-
let j = from + 1;
|
|
363
|
-
while (j < n) {
|
|
364
|
-
if (command[j] === "\\") {
|
|
365
|
-
j += 2;
|
|
366
|
-
continue;
|
|
367
|
-
}
|
|
368
|
-
if (command[j] === "`")
|
|
369
|
-
return j + 1;
|
|
370
|
-
j++;
|
|
371
|
-
}
|
|
372
|
-
return -1;
|
|
373
|
-
};
|
|
374
|
-
const scanParen = (from) => {
|
|
375
|
-
let depth = 0;
|
|
376
|
-
let j = from;
|
|
377
|
-
while (j < n) {
|
|
378
|
-
const c = command[j];
|
|
379
|
-
if (c === "\\") {
|
|
380
|
-
j += 2;
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
if (c === "'") {
|
|
384
|
-
const e = scanSingle(j);
|
|
385
|
-
if (e < 0)
|
|
386
|
-
return -1;
|
|
387
|
-
j = e;
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
390
|
-
if (c === '"') {
|
|
391
|
-
const e = scanDouble(j);
|
|
392
|
-
if (e < 0)
|
|
393
|
-
return -1;
|
|
394
|
-
j = e;
|
|
395
|
-
continue;
|
|
396
|
-
}
|
|
397
|
-
if (c === "`") {
|
|
398
|
-
const e = scanBacktick(j);
|
|
399
|
-
if (e < 0)
|
|
400
|
-
return -1;
|
|
401
|
-
j = e;
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
|
-
if (c === "(")
|
|
405
|
-
depth++;
|
|
406
|
-
else if (c === ")") {
|
|
407
|
-
depth--;
|
|
408
|
-
if (depth === 0)
|
|
409
|
-
return j + 1;
|
|
410
|
-
}
|
|
411
|
-
j++;
|
|
412
|
-
}
|
|
413
|
-
return -1;
|
|
414
|
-
};
|
|
415
|
-
function scanDouble(from) {
|
|
416
|
-
let j = from + 1;
|
|
417
|
-
while (j < n) {
|
|
418
|
-
const c = command[j];
|
|
419
|
-
if (c === "\\") {
|
|
420
|
-
j += 2;
|
|
421
|
-
continue;
|
|
422
|
-
}
|
|
423
|
-
if (c === '"')
|
|
424
|
-
return j + 1;
|
|
425
|
-
if (c === "`") {
|
|
426
|
-
const e = scanBacktick(j);
|
|
427
|
-
if (e < 0)
|
|
428
|
-
return -1;
|
|
429
|
-
j = e;
|
|
430
|
-
continue;
|
|
431
|
-
}
|
|
432
|
-
if (c === "$" && command[j + 1] === "(") {
|
|
433
|
-
const e = scanParen(j + 1);
|
|
434
|
-
if (e < 0)
|
|
435
|
-
return -1;
|
|
436
|
-
j = e;
|
|
437
|
-
continue;
|
|
438
|
-
}
|
|
439
|
-
j++;
|
|
440
|
-
}
|
|
441
|
-
return -1;
|
|
442
|
-
}
|
|
443
|
-
while (i < n) {
|
|
444
|
-
const ch = command[i];
|
|
445
|
-
if (ch === " " || ch === "\t" || ch === "\r") {
|
|
446
|
-
closeWord(i);
|
|
447
|
-
i++;
|
|
448
|
-
continue;
|
|
449
|
-
}
|
|
450
|
-
if (ch === `
|
|
451
|
-
`) {
|
|
452
|
-
closeSegment(i);
|
|
453
|
-
i++;
|
|
454
|
-
continue;
|
|
455
|
-
}
|
|
456
|
-
if (ch === ";") {
|
|
457
|
-
closeSegment(i);
|
|
458
|
-
i++;
|
|
459
|
-
continue;
|
|
460
|
-
}
|
|
461
|
-
if (ch === "&") {
|
|
462
|
-
closeSegment(i);
|
|
463
|
-
i += command[i + 1] === "&" ? 2 : 1;
|
|
464
|
-
continue;
|
|
465
|
-
}
|
|
466
|
-
if (ch === "|") {
|
|
467
|
-
closeSegment(i);
|
|
468
|
-
i += command[i + 1] === "|" ? 2 : 1;
|
|
469
|
-
continue;
|
|
470
|
-
}
|
|
471
|
-
if (ch === "(" || ch === ")") {
|
|
472
|
-
closeSegment(i);
|
|
473
|
-
i++;
|
|
474
|
-
continue;
|
|
475
|
-
}
|
|
476
|
-
if (ch === "<" && command[i + 1] === "<" && command[i + 2] !== "<") {
|
|
477
|
-
const marker = command.slice(i + 2).match(/^-?[ \t]*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1/);
|
|
478
|
-
if (!marker) {
|
|
479
|
-
trustworthy = false;
|
|
480
|
-
i = n;
|
|
481
|
-
continue;
|
|
482
|
-
}
|
|
483
|
-
const bodyStart = command.indexOf(`
|
|
484
|
-
`, i + 2 + marker[0].length);
|
|
485
|
-
if (bodyStart === -1) {
|
|
486
|
-
trustworthy = false;
|
|
487
|
-
i = n;
|
|
488
|
-
continue;
|
|
489
|
-
}
|
|
490
|
-
const rest = command.slice(bodyStart + 1);
|
|
491
|
-
const endIdx = findHeredocEnd(rest, marker[2]);
|
|
492
|
-
if (endIdx === -1) {
|
|
493
|
-
trustworthy = false;
|
|
494
|
-
i = n;
|
|
495
|
-
continue;
|
|
496
|
-
}
|
|
497
|
-
closeWord(i);
|
|
498
|
-
i = bodyStart + 1 + endIdx + marker[2].length;
|
|
499
|
-
continue;
|
|
500
|
-
}
|
|
501
|
-
if (ch === ">" || ch === "<") {
|
|
502
|
-
if (wordStart >= 0 && /^\d+$/.test(command.slice(wordStart, i)))
|
|
503
|
-
wordStart = -1;
|
|
504
|
-
closeWord(i);
|
|
505
|
-
pendingRedirect = true;
|
|
506
|
-
i++;
|
|
507
|
-
if (command[i] === ch || command[i] === "&" || ch === ">" && command[i] === "|" || ch === "<" && command[i] === ">")
|
|
508
|
-
i++;
|
|
509
|
-
continue;
|
|
510
|
-
}
|
|
511
|
-
if (ch === "'") {
|
|
512
|
-
if (wordStart < 0)
|
|
513
|
-
wordStart = i;
|
|
514
|
-
const e = scanSingle(i);
|
|
515
|
-
if (e < 0) {
|
|
516
|
-
trustworthy = false;
|
|
517
|
-
i = n;
|
|
518
|
-
} else
|
|
519
|
-
i = e;
|
|
520
|
-
continue;
|
|
521
|
-
}
|
|
522
|
-
if (ch === '"') {
|
|
523
|
-
if (wordStart < 0)
|
|
524
|
-
wordStart = i;
|
|
525
|
-
const e = scanDouble(i);
|
|
526
|
-
if (e < 0) {
|
|
527
|
-
trustworthy = false;
|
|
528
|
-
i = n;
|
|
529
|
-
} else
|
|
530
|
-
i = e;
|
|
531
|
-
continue;
|
|
532
|
-
}
|
|
533
|
-
if (ch === "`") {
|
|
534
|
-
if (wordStart < 0)
|
|
535
|
-
wordStart = i;
|
|
536
|
-
const e = scanBacktick(i);
|
|
537
|
-
if (e < 0) {
|
|
538
|
-
trustworthy = false;
|
|
539
|
-
i = n;
|
|
540
|
-
} else {
|
|
541
|
-
substitutions.push(command.slice(i + 1, e - 1));
|
|
542
|
-
i = e;
|
|
543
|
-
}
|
|
544
|
-
continue;
|
|
545
|
-
}
|
|
546
|
-
if (ch === "$" && command[i + 1] === "(") {
|
|
547
|
-
if (wordStart < 0)
|
|
548
|
-
wordStart = i;
|
|
549
|
-
const e = scanParen(i + 1);
|
|
550
|
-
if (e < 0) {
|
|
551
|
-
trustworthy = false;
|
|
552
|
-
i = n;
|
|
553
|
-
} else {
|
|
554
|
-
substitutions.push(command.slice(i + 2, e - 1));
|
|
555
|
-
i = e;
|
|
556
|
-
}
|
|
557
|
-
continue;
|
|
558
|
-
}
|
|
559
|
-
if (ch === "\\") {
|
|
560
|
-
if (wordStart < 0)
|
|
561
|
-
wordStart = i;
|
|
562
|
-
if (i + 1 >= n) {
|
|
563
|
-
trustworthy = false;
|
|
564
|
-
i = n;
|
|
565
|
-
} else
|
|
566
|
-
i += 2;
|
|
567
|
-
continue;
|
|
568
|
-
}
|
|
569
|
-
if (wordStart < 0)
|
|
570
|
-
wordStart = i;
|
|
571
|
-
i++;
|
|
572
|
-
}
|
|
573
|
-
closeSegment(n);
|
|
574
|
-
return { segments, trustworthy, substitutions };
|
|
575
|
-
}
|
|
576
|
-
function findHeredocEnd(rest, delimiter) {
|
|
577
|
-
let from = 0;
|
|
578
|
-
while (from <= rest.length) {
|
|
579
|
-
const at = rest.indexOf(delimiter, from);
|
|
580
|
-
if (at === -1)
|
|
581
|
-
return -1;
|
|
582
|
-
const lineStart = at === 0 || rest[at - 1] === `
|
|
583
|
-
`;
|
|
584
|
-
const after = at + delimiter.length;
|
|
585
|
-
const lineEnd = after === rest.length || rest[after] === `
|
|
586
|
-
` || rest[after] === "\r";
|
|
587
|
-
if (lineStart && lineEnd)
|
|
588
|
-
return at;
|
|
589
|
-
from = at + 1;
|
|
590
|
-
}
|
|
591
|
-
return -1;
|
|
592
|
-
}
|
|
593
|
-
function decodeWord(text) {
|
|
594
|
-
let out = "";
|
|
595
|
-
let i = 0;
|
|
596
|
-
while (i < text.length) {
|
|
597
|
-
const ch = text[i];
|
|
598
|
-
if (ch === "\\") {
|
|
599
|
-
if (i + 1 < text.length)
|
|
600
|
-
out += text[i + 1];
|
|
601
|
-
i += 2;
|
|
602
|
-
continue;
|
|
603
|
-
}
|
|
604
|
-
if (ch === "'") {
|
|
605
|
-
const end = text.indexOf("'", i + 1);
|
|
606
|
-
if (end === -1) {
|
|
607
|
-
out += text.slice(i + 1);
|
|
608
|
-
break;
|
|
609
|
-
}
|
|
610
|
-
out += text.slice(i + 1, end);
|
|
611
|
-
i = end + 1;
|
|
612
|
-
continue;
|
|
613
|
-
}
|
|
614
|
-
if (ch === '"') {
|
|
615
|
-
i++;
|
|
616
|
-
while (i < text.length && text[i] !== '"') {
|
|
617
|
-
if (text[i] === "\\" && i + 1 < text.length) {
|
|
618
|
-
out += text[i + 1];
|
|
619
|
-
i += 2;
|
|
620
|
-
continue;
|
|
621
|
-
}
|
|
622
|
-
out += text[i];
|
|
623
|
-
i++;
|
|
624
|
-
}
|
|
625
|
-
i++;
|
|
626
|
-
continue;
|
|
627
|
-
}
|
|
628
|
-
out += ch;
|
|
629
|
-
i++;
|
|
630
|
-
}
|
|
631
|
-
return out;
|
|
632
|
-
}
|
|
633
|
-
function commandWordIndex(segment) {
|
|
634
|
-
let index = 0;
|
|
635
|
-
let guard = 0;
|
|
636
|
-
while (index < segment.words.length && guard++ < 32) {
|
|
637
|
-
if (segment.redirects.has(index)) {
|
|
638
|
-
index++;
|
|
639
|
-
continue;
|
|
640
|
-
}
|
|
641
|
-
const raw = segment.words[index].text;
|
|
642
|
-
const decoded = decodeWord(raw);
|
|
643
|
-
if (ASSIGNMENT_RE.test(decoded) && !decoded.startsWith("-")) {
|
|
644
|
-
index++;
|
|
645
|
-
continue;
|
|
646
|
-
}
|
|
647
|
-
if (KEYWORDS.has(decoded)) {
|
|
648
|
-
index++;
|
|
649
|
-
continue;
|
|
650
|
-
}
|
|
651
|
-
const wrapper = baseVerb(decoded);
|
|
652
|
-
if (wrapper === "command") {
|
|
653
|
-
for (const word of segment.words.filter((_, at) => at > index && !segment.redirects.has(at))) {
|
|
654
|
-
const option = decodeWord(word.text);
|
|
655
|
-
if (option === "--" || !option.startsWith("-"))
|
|
656
|
-
break;
|
|
657
|
-
if (/^-[pvV]*[vV][pvV]*$/.test(option))
|
|
658
|
-
return -1;
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
const wrapperValues = WRAPPERS[wrapper];
|
|
662
|
-
if (wrapperValues) {
|
|
663
|
-
const wrapperIndex = index;
|
|
664
|
-
index++;
|
|
665
|
-
while (index < segment.words.length) {
|
|
666
|
-
if (segment.redirects.has(index)) {
|
|
667
|
-
index++;
|
|
668
|
-
continue;
|
|
669
|
-
}
|
|
670
|
-
const option = decodeWord(segment.words[index].text);
|
|
671
|
-
if (!option.startsWith("-") || option === "-")
|
|
672
|
-
break;
|
|
673
|
-
if (option === "--") {
|
|
674
|
-
index++;
|
|
675
|
-
break;
|
|
676
|
-
}
|
|
677
|
-
if (wrapper === "env" && (/^-[iv0]*S/.test(option) || /^--split-string(?:=|$)/.test(option)))
|
|
678
|
-
return wrapperIndex;
|
|
679
|
-
index++;
|
|
680
|
-
if (wrapperValues.includes(option) || wrapper === "exec" && /^-[cl]*a$/.test(option)) {
|
|
681
|
-
while (segment.redirects.has(index))
|
|
682
|
-
index++;
|
|
683
|
-
index++;
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
let positionals = WRAPPER_POSITIONALS[wrapper] ?? 0;
|
|
687
|
-
while (positionals > 0 && index < segment.words.length) {
|
|
688
|
-
const positional = decodeWord(segment.words[index].text);
|
|
689
|
-
if (positional.startsWith("-") && positional !== "-")
|
|
690
|
-
break;
|
|
691
|
-
index++;
|
|
692
|
-
positionals--;
|
|
693
|
-
}
|
|
694
|
-
continue;
|
|
695
|
-
}
|
|
696
|
-
return index;
|
|
697
|
-
}
|
|
698
|
-
return -1;
|
|
699
|
-
}
|
|
700
|
-
function rmOperands(segment, verbIndex) {
|
|
701
|
-
const operands = [];
|
|
702
|
-
let seenDoubleDash = false;
|
|
703
|
-
for (let index = verbIndex + 1;index < segment.words.length; index++) {
|
|
704
|
-
if (segment.redirects.has(index))
|
|
705
|
-
continue;
|
|
706
|
-
const decoded = decodeWord(segment.words[index].text);
|
|
707
|
-
if (!seenDoubleDash) {
|
|
708
|
-
if (decoded === "--") {
|
|
709
|
-
seenDoubleDash = true;
|
|
710
|
-
continue;
|
|
711
|
-
}
|
|
712
|
-
if (decoded.startsWith("-") && decoded !== "-")
|
|
713
|
-
continue;
|
|
714
|
-
}
|
|
715
|
-
operands.push(segment.words[index]);
|
|
716
|
-
}
|
|
717
|
-
return operands;
|
|
718
|
-
}
|
|
719
|
-
function handoffRoots(home) {
|
|
720
|
-
return HANDOFF_ROOTS.map((suffix) => join2(home, ...suffix.split("/")));
|
|
721
|
-
}
|
|
722
|
-
function expandHomeSpelling(token, home) {
|
|
723
|
-
if (token === "~" || token === "$HOME" || token === "${HOME}")
|
|
724
|
-
return home;
|
|
725
|
-
if (token.startsWith("~/"))
|
|
726
|
-
return join2(home, token.slice(2));
|
|
727
|
-
if (token.startsWith("$HOME/"))
|
|
728
|
-
return join2(home, token.slice("$HOME/".length));
|
|
729
|
-
if (token.startsWith("${HOME}/"))
|
|
730
|
-
return join2(home, token.slice("${HOME}/".length));
|
|
731
|
-
return token;
|
|
732
|
-
}
|
|
733
|
-
function resolveLiteralTarget(raw, cwd, home) {
|
|
734
|
-
let decoded = decodeWord(raw);
|
|
735
|
-
if (!decoded)
|
|
736
|
-
return null;
|
|
737
|
-
if (decoded.includes("`"))
|
|
738
|
-
return null;
|
|
739
|
-
if (decoded === "$HOME" || decoded === "${HOME}" || decoded === "$HOME/" || decoded === "${HOME}/")
|
|
740
|
-
return home;
|
|
741
|
-
const withoutHome = decoded.replace(/\$\{HOME\}/g, "").replace(/\$HOME/g, "");
|
|
742
|
-
if (withoutHome.includes("$"))
|
|
743
|
-
return null;
|
|
744
|
-
if (/[*?[\]]/.test(decoded))
|
|
745
|
-
return null;
|
|
746
|
-
if (decoded.startsWith("~")) {
|
|
747
|
-
const expanded = expandHomeSpelling(decoded, home);
|
|
748
|
-
if (expanded === decoded)
|
|
749
|
-
return null;
|
|
750
|
-
decoded = expanded;
|
|
751
|
-
}
|
|
752
|
-
const absolute = isAbsolute2(decoded) ? normalize(decoded) : resolve2(cwd, decoded);
|
|
753
|
-
const trimmed = absolute.replace(/\/+$/, "");
|
|
754
|
-
return trimmed === "" ? "/" : trimmed;
|
|
755
|
-
}
|
|
756
|
-
function isUnder(target, root) {
|
|
757
|
-
return target === root || target.startsWith(`${root}${sep}`);
|
|
758
|
-
}
|
|
759
|
-
function isScratchpadContent(target, home) {
|
|
760
|
-
const prefix = `${join2(home, ".hasna", "scratchpad", "scratch")}${sep}`;
|
|
761
|
-
if (!target.startsWith(prefix))
|
|
762
|
-
return false;
|
|
763
|
-
const [session, firstChild] = target.slice(prefix.length).split(sep);
|
|
764
|
-
if (!session || !/^[a-z0-9][a-z0-9._-]{0,63}$/.test(session) || session.endsWith(".") || !firstChild)
|
|
765
|
-
return false;
|
|
766
|
-
const control = firstChild.normalize("NFKC").toLowerCase();
|
|
767
|
-
return control !== "meta.json" && control !== ".lock";
|
|
768
|
-
}
|
|
769
|
-
function protectedTargetReason(target, home) {
|
|
770
|
-
if (target === home) {
|
|
771
|
-
return `\`${target}\` is the home directory itself, which is never deleted or trashed. Delete a specific file or directory inside it instead.`;
|
|
772
|
-
}
|
|
773
|
-
for (const root of SYSTEM_PROTECTED_ROOTS) {
|
|
774
|
-
if (target !== root && !isUnder(root, target))
|
|
775
|
-
continue;
|
|
776
|
-
return root === "/" ? `\`${target}\` is the filesystem root, which is never deleted.` : `\`${target}\` is the system root \`${root}\` (or an ancestor of it), which is never deleted or trashed.`;
|
|
777
|
-
}
|
|
778
|
-
for (const suffix of PROTECTED_HOME_TREES) {
|
|
779
|
-
const root = join2(home, suffix);
|
|
780
|
-
if (suffix === ".hasna" && isScratchpadContent(target, home))
|
|
781
|
-
continue;
|
|
782
|
-
if (!isUnder(target, root) && !isUnder(root, target))
|
|
783
|
-
continue;
|
|
784
|
-
return `\`${target}\` is the protected \`~/${suffix}\` path (or contains it), which is never deleted or trashed. Delete a specific file inside it, and only when you are sure.`;
|
|
785
|
-
}
|
|
786
|
-
return null;
|
|
787
|
-
}
|
|
788
|
-
function isHandoff(operands, cwd, home) {
|
|
789
|
-
const roots = handoffRoots(home);
|
|
790
|
-
if (roots.some((root) => isUnder(cwd, root)))
|
|
791
|
-
return true;
|
|
792
|
-
for (const operand of operands) {
|
|
793
|
-
const target = resolveLiteralTarget(operand.text, cwd, home);
|
|
794
|
-
if (target && roots.some((root) => isUnder(target, root)))
|
|
795
|
-
return true;
|
|
796
|
-
}
|
|
797
|
-
return false;
|
|
798
|
-
}
|
|
799
|
-
function scanCommand(command, options) {
|
|
800
|
-
const ctx = { cwd: options.cwd, home: options.home, depth: options.depth ?? 0 };
|
|
801
|
-
const result = { rmHits: [], handoffHits: [], refusals: [], protectedHits: [], trustworthy: true };
|
|
802
|
-
const lexed = lexCommand(command);
|
|
803
|
-
result.trustworthy = lexed.trustworthy;
|
|
804
|
-
if (!lexed.trustworthy) {
|
|
805
|
-
return result;
|
|
806
|
-
}
|
|
807
|
-
let cwd = ctx.cwd;
|
|
808
|
-
for (const segment of lexed.segments) {
|
|
809
|
-
const index = commandWordIndex(segment);
|
|
810
|
-
if (index < 0)
|
|
811
|
-
continue;
|
|
812
|
-
const verb = baseVerb(decodeWord(segment.words[index].text));
|
|
813
|
-
if (verb === "cd") {
|
|
814
|
-
const operand = segment.words[index + 1];
|
|
815
|
-
if (operand) {
|
|
816
|
-
const target = resolveLiteralTarget(operand.text, cwd, ctx.home);
|
|
817
|
-
if (target)
|
|
818
|
-
cwd = target;
|
|
819
|
-
else
|
|
820
|
-
cwd = ctx.cwd;
|
|
821
|
-
}
|
|
822
|
-
continue;
|
|
823
|
-
}
|
|
824
|
-
const refusal = REFUSED_VERBS[verb];
|
|
825
|
-
if (refusal) {
|
|
826
|
-
result.refusals.push({ reason: refusal });
|
|
827
|
-
continue;
|
|
828
|
-
}
|
|
829
|
-
if (verb === "rm") {
|
|
830
|
-
const operands = rmOperands(segment, index);
|
|
831
|
-
if (operands.length === 0)
|
|
832
|
-
continue;
|
|
833
|
-
const hit = { word: segment.words[index], operands };
|
|
834
|
-
if (isHandoff(hit.operands, cwd, ctx.home)) {
|
|
835
|
-
result.handoffHits.push(hit);
|
|
836
|
-
continue;
|
|
837
|
-
}
|
|
838
|
-
result.rmHits.push(hit);
|
|
839
|
-
for (const operand of hit.operands) {
|
|
840
|
-
const target = resolveLiteralTarget(operand.text, cwd, ctx.home);
|
|
841
|
-
if (!target)
|
|
842
|
-
continue;
|
|
843
|
-
const reason = protectedTargetReason(target, ctx.home);
|
|
844
|
-
if (reason) {
|
|
845
|
-
result.protectedHits.push({ reason });
|
|
846
|
-
break;
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
continue;
|
|
850
|
-
}
|
|
851
|
-
if (APPLET_DISPATCHERS.has(verb)) {
|
|
852
|
-
const appletIndex = segment.words.findIndex((word, at) => at > index && !decodeWord(word.text).startsWith("-"));
|
|
853
|
-
const applet = appletIndex >= 0 ? baseVerb(decodeWord(segment.words[appletIndex].text)) : "";
|
|
854
|
-
if (applet === "rm" || applet in REFUSED_VERBS) {
|
|
855
|
-
result.refusals.push({
|
|
856
|
-
reason: `\`${verb} ${applet}\` runs the ${verb} applet, whose own \`rm\` is not GNU \`rm\` and cannot be rewritten in place. Run the delete as \`rm -- <path>\`, which is redirected into trash.`
|
|
857
|
-
});
|
|
858
|
-
}
|
|
859
|
-
continue;
|
|
860
|
-
}
|
|
861
|
-
if (verb === "git") {
|
|
862
|
-
const sub = gitSubcommand(segment, index);
|
|
863
|
-
if (sub === "rm" || sub === "clean") {
|
|
864
|
-
const dryOrIndexOnly = sub === "rm" ? gitRmIsNonDeleting(segment, index) : gitHasDryRun(segment, index);
|
|
865
|
-
if (!dryOrIndexOnly) {
|
|
866
|
-
result.refusals.push({
|
|
867
|
-
reason: sub === "rm" ? "`git rm` is refused: without `--cached` it deletes the working-tree file, and its semantics are not `rm`'s. Use `rm -- <path>` (intercepted and redirected into trash) to remove the file, or `git rm --cached -- <path>` to unstage it without deleting." : "`git clean` deletes untracked files irrecoverably and is refused. Remove them explicitly with `rm -- <path>` (intercepted and redirected into trash), or preview with `git clean -n`."
|
|
868
|
-
});
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
continue;
|
|
872
|
-
}
|
|
873
|
-
if (verb === "find") {
|
|
874
|
-
const words = segment.words.map((word) => decodeWord(word.text));
|
|
875
|
-
const deleteIndex = words.indexOf("-delete");
|
|
876
|
-
if (deleteIndex >= 0) {
|
|
877
|
-
result.refusals.push({
|
|
878
|
-
reason: "`find -delete` deletes files irrecoverably and is refused. List the paths first (`find \u2026 -print`), then remove them explicitly with `rm -- <path>` so the delete is redirected into trash."
|
|
879
|
-
});
|
|
880
|
-
continue;
|
|
881
|
-
}
|
|
882
|
-
const execIndex = words.findIndex((word) => word === "-exec" || word === "-execdir" || word === "-ok");
|
|
883
|
-
if (execIndex >= 0 && words.slice(execIndex).some((word) => word === "rm" || word === "rmdir" || word === "unlink" || word === "shred")) {
|
|
884
|
-
result.refusals.push({
|
|
885
|
-
reason: "`find -exec <delete>` is refused: the delete runs through find, not through `rm`, and cannot be redirected into trash. Remove the paths explicitly with `rm -- <path>`."
|
|
886
|
-
});
|
|
887
|
-
continue;
|
|
888
|
-
}
|
|
889
|
-
continue;
|
|
890
|
-
}
|
|
891
|
-
if (verb === "xargs" && segment.words.some((word) => decodeWord(word.text) === "rm")) {
|
|
892
|
-
result.refusals.push({
|
|
893
|
-
reason: "`xargs rm` takes its targets from stdin, so this hook cannot verify what would be deleted. Pipe the paths into an explicit `rm -- <path> \u2026` instead, which is redirected into trash."
|
|
894
|
-
});
|
|
895
|
-
continue;
|
|
896
|
-
}
|
|
897
|
-
const opaqueText = verb === "env" ? envSplitCommandText(segment.words.map((word) => decodeWord(word.text))) : segmentText(segment);
|
|
898
|
-
if (OPAQUE_COMMANDS.has(verb) && mentionsDeleteVerb(opaqueText)) {
|
|
899
|
-
result.refusals.push({
|
|
900
|
-
reason: `\`${verb}\` runs a command string this hook cannot rewrite in place. Run the delete directly as \`rm -- <path>\` (intercepted and redirected into trash), or as a script that uses \`trash guard\` itself.`
|
|
901
|
-
});
|
|
902
|
-
continue;
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
if (ctx.depth < 3) {
|
|
906
|
-
for (const body of lexed.substitutions) {
|
|
907
|
-
const inner = scanCommand(body, { cwd, home: ctx.home, depth: ctx.depth + 1 });
|
|
908
|
-
if (inner.rmHits.length > 0 || inner.refusals.length > 0 || inner.handoffHits.length > 0 || inner.protectedHits.length > 0) {
|
|
909
|
-
result.refusals.push({
|
|
910
|
-
reason: "This command runs a delete inside a command substitution `$(\u2026)`, which this hook cannot rewrite. Run the delete as a separate `rm -- <path>` command, which is redirected into trash."
|
|
911
|
-
});
|
|
912
|
-
break;
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
} else if (lexed.substitutions.some((body) => mentionsDeleteVerb(body))) {
|
|
916
|
-
result.refusals.push({
|
|
917
|
-
reason: "This command nests a delete inside command substitutions deeper than this hook will follow. Run the delete as a separate `rm -- <path>` command."
|
|
918
|
-
});
|
|
919
|
-
}
|
|
920
|
-
return result;
|
|
921
|
-
}
|
|
922
|
-
function segmentText(segment) {
|
|
923
|
-
return segment.words.map((word) => word.text).join(" ");
|
|
924
|
-
}
|
|
925
|
-
function gitSubcommand(segment, verbIndex) {
|
|
926
|
-
let index = verbIndex + 1;
|
|
927
|
-
while (index < segment.words.length) {
|
|
928
|
-
const option = decodeWord(segment.words[index].text);
|
|
929
|
-
if (!option.startsWith("-"))
|
|
930
|
-
return option;
|
|
931
|
-
index++;
|
|
932
|
-
if (GIT_VALUE_OPTIONS.has(option))
|
|
933
|
-
index++;
|
|
934
|
-
}
|
|
935
|
-
return null;
|
|
936
|
-
}
|
|
937
|
-
function gitRmIsNonDeleting(segment, verbIndex) {
|
|
938
|
-
const options = segment.words.slice(verbIndex + 1).map((word) => decodeWord(word.text));
|
|
939
|
-
if (options.includes("--cached"))
|
|
940
|
-
return true;
|
|
941
|
-
return gitHasDryRun(segment, verbIndex);
|
|
942
|
-
}
|
|
943
|
-
function gitHasDryRun(segment, verbIndex) {
|
|
944
|
-
const options = segment.words.slice(verbIndex + 1).map((word) => decodeWord(word.text));
|
|
945
|
-
return options.includes("-n") || options.includes("--dry-run");
|
|
946
|
-
}
|
|
947
|
-
function shellQuoteWord(word) {
|
|
948
|
-
if (/^[A-Za-z0-9_\-./+=:,@%^]+$/.test(word))
|
|
949
|
-
return word;
|
|
950
|
-
return `'${word.replace(/'/g, `'\\''`)}'`;
|
|
951
|
-
}
|
|
952
|
-
function rewriteCommand(command, hits, trashPath, execution) {
|
|
953
|
-
const replacement = execution ? [
|
|
954
|
-
"/usr/bin/env",
|
|
955
|
-
"-i",
|
|
956
|
-
`HOME=${execution.home}`,
|
|
957
|
-
`PATH=${execution.path}`,
|
|
958
|
-
`HASNA_TRASH_AGENT=${execution.agent}`,
|
|
959
|
-
execution.runtime,
|
|
960
|
-
"--config=/dev/null",
|
|
961
|
-
"--no-env-file",
|
|
962
|
-
"--no-macros",
|
|
963
|
-
"--no-install",
|
|
964
|
-
trashPath,
|
|
965
|
-
"guard"
|
|
966
|
-
].map(shellQuoteWord).join(" ") : `${shellQuoteWord(trashPath)} guard`;
|
|
967
|
-
const ordered = [...hits].sort((a, b) => b.word.start - a.word.start);
|
|
968
|
-
let out = command;
|
|
969
|
-
for (const hit of ordered) {
|
|
970
|
-
out = `${out.slice(0, hit.word.start)}${replacement}${out.slice(hit.word.end)}`;
|
|
971
|
-
}
|
|
972
|
-
return out;
|
|
973
|
-
}
|
|
974
|
-
function rewriteIsComplete(original, rewritten, hits, cwd, home) {
|
|
975
|
-
if (!rewritten || rewritten === original)
|
|
976
|
-
return false;
|
|
977
|
-
if (hits.length === 0)
|
|
978
|
-
return false;
|
|
979
|
-
const verify = scanCommand(rewritten, { cwd, home });
|
|
980
|
-
if (!verify.trustworthy)
|
|
981
|
-
return false;
|
|
982
|
-
if (verify.rmHits.length > 0 || verify.handoffHits.length > 0)
|
|
983
|
-
return false;
|
|
984
|
-
return verify.refusals.length === 0;
|
|
985
|
-
}
|
|
986
|
-
function buildUpdatedInput(toolInput, command) {
|
|
987
|
-
const out = {};
|
|
988
|
-
for (const [key, value] of Object.entries(toolInput ?? {}))
|
|
989
|
-
out[key] = value;
|
|
990
|
-
out.command = command;
|
|
991
|
-
if (typeof out.description !== "string" || out.description.trim() === "") {
|
|
992
|
-
out.description = DEFAULT_DESCRIPTION;
|
|
993
|
-
}
|
|
994
|
-
if (typeof out.timeout !== "number" || !Number.isFinite(out.timeout) || out.timeout <= 0) {
|
|
995
|
-
out.timeout = DEFAULT_TIMEOUT_MS;
|
|
996
|
-
}
|
|
997
|
-
if (typeof out.run_in_background !== "boolean") {
|
|
998
|
-
out.run_in_background = false;
|
|
999
|
-
}
|
|
1000
|
-
if ("dangerouslyDisableSandbox" in out && typeof out.dangerouslyDisableSandbox !== "boolean") {
|
|
1001
|
-
delete out.dangerouslyDisableSandbox;
|
|
1002
|
-
}
|
|
1003
|
-
return out;
|
|
1004
|
-
}
|
|
1005
|
-
function updatedInputIsComplete(updated, original, hits, cwd, home) {
|
|
1006
|
-
if (typeof updated.command !== "string" || updated.command.trim() === "")
|
|
1007
|
-
return false;
|
|
1008
|
-
if (typeof updated.description !== "string" || updated.description.trim() === "")
|
|
1009
|
-
return false;
|
|
1010
|
-
if (typeof updated.timeout !== "number" || !Number.isFinite(updated.timeout) || updated.timeout <= 0)
|
|
1011
|
-
return false;
|
|
1012
|
-
if (typeof updated.run_in_background !== "boolean")
|
|
1013
|
-
return false;
|
|
1014
|
-
if ("dangerouslyDisableSandbox" in updated && typeof updated.dangerouslyDisableSandbox !== "boolean")
|
|
1015
|
-
return false;
|
|
1016
|
-
return rewriteIsComplete(original, updated.command, hits, cwd, home);
|
|
1017
|
-
}
|
|
1018
|
-
function changesExecutionEnvironment(command) {
|
|
1019
|
-
const stateCommands = new Set(["export", "set", "unset", "readonly", "declare", "typeset", "local", "alias", "unalias", "read", "readarray", "mapfile", "getopts", "trap", "enable", "function", "env", "sudo", "doas", "stdbuf"]);
|
|
1020
|
-
const parsed = lexCommand(command);
|
|
1021
|
-
return parsed.segments.some((segment) => {
|
|
1022
|
-
const index = commandWordIndex(segment);
|
|
1023
|
-
const prefix = segment.words.slice(0, index < 0 ? segment.words.length : index);
|
|
1024
|
-
if (prefix.some((word2) => ASSIGNMENT_RE.test(decodeWord(word2.text)) || stateCommands.has(baseVerb(decodeWord(word2.text)))))
|
|
1025
|
-
return true;
|
|
1026
|
-
if (index < 0)
|
|
1027
|
-
return false;
|
|
1028
|
-
const word = segment.words[index];
|
|
1029
|
-
return stateCommands.has(baseVerb(decodeWord(word.text))) || /^\s*\(\s*\)\s*\{/.test(command.slice(word.end));
|
|
1030
|
-
});
|
|
1031
|
-
}
|
|
1032
|
-
function deny(reason) {
|
|
1033
|
-
return {
|
|
1034
|
-
hookSpecificOutput: {
|
|
1035
|
-
hookEventName: "PreToolUse",
|
|
1036
|
-
permissionDecision: "deny",
|
|
1037
|
-
permissionDecisionReason: reason
|
|
1038
|
-
}
|
|
1039
|
-
};
|
|
1040
|
-
}
|
|
1041
|
-
function allowRewrite(updatedInput) {
|
|
1042
|
-
return {
|
|
1043
|
-
hookSpecificOutput: {
|
|
1044
|
-
hookEventName: "PreToolUse",
|
|
1045
|
-
permissionDecision: "allow",
|
|
1046
|
-
updatedInput
|
|
1047
|
-
}
|
|
1048
|
-
};
|
|
1049
|
-
}
|
|
1050
|
-
function resolveTrash(deps) {
|
|
1051
|
-
try {
|
|
1052
|
-
if (deps.inspectTrash) {
|
|
1053
|
-
const inspection = deps.inspectTrash();
|
|
1054
|
-
return inspection.ok ? { path: inspection.path, error: null } : { path: null, error: `${inspection.code}: ${BINARY_FAILURES[inspection.code]}` };
|
|
1055
|
-
}
|
|
1056
|
-
return { path: deps.findTrash(), error: null };
|
|
1057
|
-
} catch (cause) {
|
|
1058
|
-
return { path: null, error: cause instanceof Error ? cause.message : String(cause) };
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
var ABSENT_REASON = "[trash-guard] No verified @hasna/trash guard was found on PATH. This deletion is refused. Install the current @hasna/trash package with Bun and run its setup; the operating-system trash utility is not a compatible guard.";
|
|
1062
|
-
function evaluate(input, deps) {
|
|
1063
|
-
if (input.hook_event_name !== "PreToolUse")
|
|
1064
|
-
return { continue: true };
|
|
1065
|
-
if (["apply_patch", "ApplyPatch", "functions.apply_patch"].includes(input.tool_name ?? "")) {
|
|
1066
|
-
const patch = input.tool_input?.command;
|
|
1067
|
-
if (typeof patch !== "string")
|
|
1068
|
-
return deny("[trash-guard] Unreadable patch input; deletion safety cannot be checked.");
|
|
1069
|
-
if (/^\s*\*\*\* Delete File:/m.test(patch)) {
|
|
1070
|
-
return deny("[trash-guard] Delete File would bypass recoverable deletion. First use `trash put -- <path>` or the trash_put MCP tool, then submit any remaining edits without the deletion block.");
|
|
1071
|
-
}
|
|
1072
|
-
return { continue: true };
|
|
1073
|
-
}
|
|
1074
|
-
if (input.tool_name !== "Bash")
|
|
1075
|
-
return { continue: true };
|
|
1076
|
-
const rawCommand = input.tool_input?.command;
|
|
1077
|
-
if (typeof rawCommand !== "string") {
|
|
1078
|
-
return deny("[trash-guard] The Bash tool call carried no readable `command`, so this hook cannot verify what would run. Refusing a call it cannot verify. Re-run the delete explicitly as `rm -- <path>` so it can be redirected into trash.");
|
|
1079
|
-
}
|
|
1080
|
-
const command = getCommand(input);
|
|
1081
|
-
if (!command.trim())
|
|
1082
|
-
return { continue: true };
|
|
1083
|
-
const scanned = scanCommand(command, { cwd: deps.cwd, home: deps.home });
|
|
1084
|
-
if (scanned.refusals.length > 0) {
|
|
1085
|
-
return deny(`[trash-guard] ${scanned.refusals[0].reason}`);
|
|
1086
|
-
}
|
|
1087
|
-
if (scanned.protectedHits.length > 0) {
|
|
1088
|
-
return deny(`[trash-guard] ${scanned.protectedHits[0].reason}`);
|
|
1089
|
-
}
|
|
1090
|
-
if (!scanned.trustworthy) {
|
|
1091
|
-
return mentionsDeleteVerb(command) ? deny("[trash-guard] This command could not be parsed to the end (unterminated quote, substitution or here-document), so a delete inside it cannot be redirected. Re-run the delete as a plain `rm -- <path>` command.") : { continue: true };
|
|
1092
|
-
}
|
|
1093
|
-
if (scanned.rmHits.length === 0) {
|
|
1094
|
-
if (scanned.handoffHits.length > 0) {
|
|
1095
|
-
return { continue: true };
|
|
1096
|
-
}
|
|
1097
|
-
return { continue: true };
|
|
1098
|
-
}
|
|
1099
|
-
if (scanned.handoffHits.length > 0) {
|
|
1100
|
-
return deny("[trash-guard] This command mixes a delete under the protected repo-checkout roots (owned by workspace-repos-guard) with a delete outside them, and a partial rewrite would leave one of them unredirected. Re-run them as separate commands, so each delete is handled by the hook that owns it.");
|
|
1101
|
-
}
|
|
1102
|
-
const trash = resolveTrash(deps);
|
|
1103
|
-
if (trash.error !== null) {
|
|
1104
|
-
return deny(`[trash-guard] The guard could not resolve the \`trash\` binary (${trash.error}), so this \`rm\` cannot be redirected into a recoverable delete \u2014 and an unrecoverable delete is never allowed, so the command is refused rather than run. Fix the environment (PATH, permissions) and re-run the same command.`);
|
|
1105
|
-
}
|
|
1106
|
-
if (trash.path === null) {
|
|
1107
|
-
return deny(ABSENT_REASON);
|
|
1108
|
-
}
|
|
1109
|
-
const trashPath = trash.path;
|
|
1110
|
-
if (deps.execution && changesExecutionEnvironment(command)) {
|
|
1111
|
-
return deny("[trash-guard] A deletion combined with shell environment changes cannot use the verified native execution context. Run the deletion as a separate plain rm command.");
|
|
1112
|
-
}
|
|
1113
|
-
const rewritten = rewriteCommand(command, scanned.rmHits, trashPath, deps.execution);
|
|
1114
|
-
const updatedInput = buildUpdatedInput(input.tool_input, rewritten);
|
|
1115
|
-
if (deps.execution)
|
|
1116
|
-
updatedInput.hasna_execution = "hasna.hooks.execution.v1";
|
|
1117
|
-
if (!updatedInputIsComplete(updatedInput, command, scanned.rmHits, deps.cwd, deps.home)) {
|
|
1118
|
-
return deny("[trash-guard] The rewrite of this command did not verify (the delete could not be redirected cleanly), so the command is refused rather than run unredirected. Re-run the delete as a plain `rm -- <path>` command.");
|
|
1119
|
-
}
|
|
1120
|
-
return allowRewrite(updatedInput);
|
|
1121
|
-
}
|
|
1122
|
-
if (false) {}
|
|
1123
|
-
|
|
1124
|
-
// hooks/hook-workspace-repos-guard/src/hook.ts
|
|
1125
|
-
import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSync2, lstatSync as lstatSync2, openSync as openSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
1126
|
-
import { homedir } from "os";
|
|
1127
|
-
import { dirname as dirname2, isAbsolute as isAbsolute3, join as join3, normalize as normalize2, relative, resolve as resolve3, sep as sep2 } from "path";
|
|
1128
|
-
var FILE_WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
|
|
1129
|
-
var PATCH_TOOLS = new Set(["apply_patch", "ApplyPatch", "functions.apply_patch"]);
|
|
1130
|
-
var DEFAULT_ORGS = ["hasna", "hasnaxyz", "hasna-products"];
|
|
1131
|
-
var RULE = "workspace-repos-guard";
|
|
1132
|
-
function reposRoot(home) {
|
|
1133
|
-
return join3(home, ".hasna", "repos", "clones");
|
|
1134
|
-
}
|
|
1135
|
-
function legacyRoot(home) {
|
|
1136
|
-
return join3(home, "workspace", "repos");
|
|
1137
|
-
}
|
|
1138
|
-
function protectedRoots(home) {
|
|
1139
|
-
return [reposRoot(home), legacyRoot(home)];
|
|
1140
|
-
}
|
|
1141
|
-
function allowedOrgsFile(home) {
|
|
1142
|
-
return join3(home, ".hasna", "hooks", "config", "workspace-repos-guard-orgs");
|
|
1143
|
-
}
|
|
1144
|
-
var ORG_NAME = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
|
|
1145
|
-
function ownedAndClosed(stat) {
|
|
1146
|
-
const uid = process.getuid?.();
|
|
1147
|
-
return uid !== undefined && (stat.uid === 0 || stat.uid === uid) && (stat.mode & 18) === 0;
|
|
1148
|
-
}
|
|
1149
|
-
function readAllowedOrgsFile(home) {
|
|
1150
|
-
const file = allowedOrgsFile(home);
|
|
1151
|
-
let text;
|
|
1152
|
-
try {
|
|
1153
|
-
const fd = openSync2(file, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
1154
|
-
try {
|
|
1155
|
-
const stat = fstatSync2(fd);
|
|
1156
|
-
if (!stat.isFile() || !ownedAndClosed(stat) || stat.size > 4096)
|
|
1157
|
-
return null;
|
|
1158
|
-
text = readFileSync2(fd, "utf8");
|
|
1159
|
-
} finally {
|
|
1160
|
-
closeSync2(fd);
|
|
1161
|
-
}
|
|
1162
|
-
const top = normalize2(home);
|
|
1163
|
-
for (let dir = dirname2(file);dir !== top; dir = dirname2(dir)) {
|
|
1164
|
-
const stat = lstatSync2(dir);
|
|
1165
|
-
if (!stat.isDirectory() || !ownedAndClosed(stat) || dir === dirname2(dir))
|
|
1166
|
-
return null;
|
|
1167
|
-
}
|
|
1168
|
-
if (!ownedAndClosed(statSync2(top)))
|
|
1169
|
-
return null;
|
|
1170
|
-
} catch {
|
|
1171
|
-
return null;
|
|
1172
|
-
}
|
|
1173
|
-
const names = text.replace(/#[^\n]*/g, "").split(/[\s,]+/).filter(Boolean);
|
|
1174
|
-
return names.length > 0 && names.every((name) => ORG_NAME.test(name)) ? new Set(names) : null;
|
|
1175
|
-
}
|
|
1176
|
-
function resolveAllowedOrgs(env = process.env, home = homedir()) {
|
|
1177
|
-
const raw = env.WORKSPACE_REPOS_GUARD_ORGS;
|
|
1178
|
-
if (typeof raw === "string" && raw.trim()) {
|
|
1179
|
-
return new Set(raw.split(",").map((s) => s.trim()).filter(Boolean));
|
|
1180
|
-
}
|
|
1181
|
-
return readAllowedOrgsFile(home) ?? new Set(DEFAULT_ORGS);
|
|
1182
|
-
}
|
|
1183
|
-
function classifyPath(target, root, orgs, op) {
|
|
1184
|
-
if (op === "read")
|
|
1185
|
-
return { blocked: false };
|
|
1186
|
-
let rel = relative(root, target);
|
|
1187
|
-
if (rel === "") {
|
|
1188
|
-
if (op === "delete")
|
|
1189
|
-
return { blocked: true, reason: `[${RULE}] delete of the protected repo-checkout root is forbidden: ${root}` };
|
|
1190
|
-
return { blocked: true, reason: `[${RULE}] writes directly to the protected repo-checkout root are forbidden: ${root}` };
|
|
1191
|
-
}
|
|
1192
|
-
if (rel.startsWith(`..${sep2}`) || rel === ".." || isAbsolute3(rel)) {
|
|
1193
|
-
return { blocked: false };
|
|
1194
|
-
}
|
|
1195
|
-
const segments = rel.split(sep2).filter(Boolean);
|
|
1196
|
-
if (op === "delete") {
|
|
1197
|
-
return { blocked: true, reason: `[${RULE}] delete under the protected repo-checkout root (${root}) is forbidden at any depth: ${target}` };
|
|
1198
|
-
}
|
|
1199
|
-
if (segments.length <= 1) {
|
|
1200
|
-
return {
|
|
1201
|
-
blocked: true,
|
|
1202
|
-
reason: `[${RULE}] writing would create a top-level entry directly under the protected repo-checkout root (${root}); only GitHub-org folders belong there: ${target}`
|
|
1203
|
-
};
|
|
1204
|
-
}
|
|
1205
|
-
const org = segments[0];
|
|
1206
|
-
if (!orgs.has(org)) {
|
|
1207
|
-
return {
|
|
1208
|
-
blocked: true,
|
|
1209
|
-
reason: `[${RULE}] '${org}' is not an allowed GitHub org under the protected repo-checkout root (${root}); allowed: ${[...orgs].join(", ")}: ${target}`
|
|
1210
|
-
};
|
|
1211
|
-
}
|
|
1212
|
-
return { blocked: false };
|
|
1213
|
-
}
|
|
1214
|
-
var WRITE_FLAGS = /\b(?:-o|--output|-O|--output-document|-out)\b/;
|
|
1215
|
-
var FD_DUPLICATION = /\d*[<>]&\s*(?:\d+|-)(?=$|[\s;&|)])/g;
|
|
1216
|
-
var OUTPUT_REDIRECT = /(?:\d+|&)?(?:>>|>\||>&|<>|>)\s*([^\s;&|<>()\x60]*)/g;
|
|
1217
|
-
function withoutFdDuplication(segment) {
|
|
1218
|
-
return segment.replace(FD_DUPLICATION, " ");
|
|
1219
|
-
}
|
|
1220
|
-
function pipelineStages(segment) {
|
|
1221
|
-
const stages = [];
|
|
1222
|
-
let quote = null;
|
|
1223
|
-
let parens = 0;
|
|
1224
|
-
let braces = 0;
|
|
1225
|
-
let backtick = false;
|
|
1226
|
-
let start = 0;
|
|
1227
|
-
for (let i = 0;i < segment.length; i++) {
|
|
1228
|
-
const ch = segment[i];
|
|
1229
|
-
if (quote === "'") {
|
|
1230
|
-
if (ch === "'")
|
|
1231
|
-
quote = null;
|
|
1232
|
-
continue;
|
|
1233
|
-
}
|
|
1234
|
-
if (ch === "\\") {
|
|
1235
|
-
i++;
|
|
1236
|
-
continue;
|
|
1237
|
-
}
|
|
1238
|
-
if (ch === "(") {
|
|
1239
|
-
parens++;
|
|
1240
|
-
continue;
|
|
1241
|
-
}
|
|
1242
|
-
if (ch === ")") {
|
|
1243
|
-
if (--parens < 0)
|
|
1244
|
-
return [segment];
|
|
1245
|
-
continue;
|
|
1246
|
-
}
|
|
1247
|
-
if (ch === "$" && segment[i + 1] === "{") {
|
|
1248
|
-
braces++;
|
|
1249
|
-
i++;
|
|
1250
|
-
continue;
|
|
1251
|
-
}
|
|
1252
|
-
if (ch === "}" && braces > 0) {
|
|
1253
|
-
braces--;
|
|
1254
|
-
continue;
|
|
1255
|
-
}
|
|
1256
|
-
if (ch === "`") {
|
|
1257
|
-
backtick = !backtick;
|
|
1258
|
-
continue;
|
|
1259
|
-
}
|
|
1260
|
-
if (quote === '"') {
|
|
1261
|
-
if (ch === '"')
|
|
1262
|
-
quote = null;
|
|
1263
|
-
continue;
|
|
1264
|
-
}
|
|
1265
|
-
if (ch === "'" || ch === '"') {
|
|
1266
|
-
quote = ch;
|
|
1267
|
-
continue;
|
|
1268
|
-
}
|
|
1269
|
-
if (ch !== "|" || parens > 0 || braces > 0 || backtick || segment[i - 1] === ">")
|
|
1270
|
-
continue;
|
|
1271
|
-
stages.push(segment.slice(start, i));
|
|
1272
|
-
if (segment[i + 1] === "&")
|
|
1273
|
-
i++;
|
|
1274
|
-
start = i + 1;
|
|
1275
|
-
}
|
|
1276
|
-
if (quote || parens !== 0 || braces !== 0 || backtick)
|
|
1277
|
-
return [segment];
|
|
1278
|
-
stages.push(segment.slice(start));
|
|
1279
|
-
return stages;
|
|
1280
|
-
}
|
|
1281
|
-
function shellWords(command) {
|
|
1282
|
-
const words = [];
|
|
1283
|
-
let text = "";
|
|
1284
|
-
let raw = "";
|
|
1285
|
-
let quote = null;
|
|
1286
|
-
let inWord = false;
|
|
1287
|
-
const flush = () => {
|
|
1288
|
-
if (!inWord)
|
|
1289
|
-
return;
|
|
1290
|
-
const body = raw.replace(/^"?\$\{?HOME\}?"?(?=\/|$)/, "");
|
|
1291
|
-
const literalTilde = text.startsWith("~") && !/^~(?:\/|$)/.test(raw);
|
|
1292
|
-
words.push({ text: literalTilde ? `./${text}` : text, opaque: /[$\x60*?[]/.test(body) || /[()]/.test(body) });
|
|
1293
|
-
text = "";
|
|
1294
|
-
raw = "";
|
|
1295
|
-
inWord = false;
|
|
1296
|
-
};
|
|
1297
|
-
for (let i = 0;i < command.length; i++) {
|
|
1298
|
-
const ch = command[i];
|
|
1299
|
-
if (quote === "'") {
|
|
1300
|
-
if (ch === "'")
|
|
1301
|
-
quote = null;
|
|
1302
|
-
else
|
|
1303
|
-
text += ch;
|
|
1304
|
-
raw += ch;
|
|
1305
|
-
continue;
|
|
1306
|
-
}
|
|
1307
|
-
if (ch === "\\" && i + 1 < command.length) {
|
|
1308
|
-
const next = command[i + 1];
|
|
1309
|
-
text += quote === '"' && !/[$\x60"\\\n]/.test(next) ? ch + next : next;
|
|
1310
|
-
raw += ch + next;
|
|
1311
|
-
inWord = true;
|
|
1312
|
-
i++;
|
|
1313
|
-
continue;
|
|
1314
|
-
}
|
|
1315
|
-
if (quote === '"') {
|
|
1316
|
-
if (ch === '"')
|
|
1317
|
-
quote = null;
|
|
1318
|
-
else
|
|
1319
|
-
text += ch;
|
|
1320
|
-
raw += ch;
|
|
1321
|
-
continue;
|
|
1322
|
-
}
|
|
1323
|
-
if (/\s/.test(ch)) {
|
|
1324
|
-
flush();
|
|
1325
|
-
continue;
|
|
1326
|
-
}
|
|
1327
|
-
inWord = true;
|
|
1328
|
-
raw += ch;
|
|
1329
|
-
if (ch === "'" || ch === '"')
|
|
1330
|
-
quote = ch;
|
|
1331
|
-
else
|
|
1332
|
-
text += ch;
|
|
1333
|
-
}
|
|
1334
|
-
if (quote) {
|
|
1335
|
-
raw += "$";
|
|
1336
|
-
}
|
|
1337
|
-
flush();
|
|
1338
|
-
return words;
|
|
1339
|
-
}
|
|
1340
|
-
function segmentOperation(segment) {
|
|
1341
|
-
const trimmed = segment.trim();
|
|
1342
|
-
if (!trimmed)
|
|
1343
|
-
return "read";
|
|
1344
|
-
const parsed = lexCommand(trimmed);
|
|
1345
|
-
if (!parsed.trustworthy || parsed.substitutions.length || /\$\{[^}]*[|;()]|[<>]\(/.test(trimmed))
|
|
1346
|
-
return opaqueOperation(trimmed);
|
|
1347
|
-
const operations = parsed.segments.map((part) => {
|
|
1348
|
-
const index = commandWordIndex(part);
|
|
1349
|
-
if (index < 0)
|
|
1350
|
-
return "read";
|
|
1351
|
-
const words = part.words.filter((_, at) => at >= index && !part.redirects.has(at)).map((word) => decodeWord(word.text));
|
|
1352
|
-
const tool = words[0].split("/").pop();
|
|
1353
|
-
if (["rm", "rmdir", "unlink", "shred", "rmtree", "del"].includes(tool ?? ""))
|
|
1354
|
-
return "delete";
|
|
1355
|
-
if (tool === "trash") {
|
|
1356
|
-
let at = 1;
|
|
1357
|
-
while (at < words.length && words[at].startsWith("-")) {
|
|
1358
|
-
const flag = words[at++];
|
|
1359
|
-
if (["--help", "-h", "--version", "-V", "--identity"].includes(flag))
|
|
1360
|
-
return "read";
|
|
1361
|
-
if (["--agent", "--spool", "--files", "--info", "--config"].includes(flag))
|
|
1362
|
-
at++;
|
|
1363
|
-
else if (!["--json", "--local"].includes(flag))
|
|
1364
|
-
return "delete";
|
|
1365
|
-
}
|
|
1366
|
-
if (at === words.length || ["status", "doctor", "list", "info", "get", "pending", "station", "setup", "backup", "storage", "retrieval", "backup-storage", "backup-inspect", "backup-refresh", "help"].includes(words[at]))
|
|
1367
|
-
return "read";
|
|
1368
|
-
if (["restore", "restore-capsule"].includes(words[at]))
|
|
1369
|
-
return "write";
|
|
1370
|
-
return "delete";
|
|
1371
|
-
}
|
|
1372
|
-
if (tool === "git") {
|
|
1373
|
-
const sub = gitSubcommand(part, index);
|
|
1374
|
-
if (sub === "clean" || sub === "rm")
|
|
1375
|
-
return "delete";
|
|
1376
|
-
return sub === "clone" || sub === "init" ? "write" : "read";
|
|
1377
|
-
}
|
|
1378
|
-
if (tool === "env" && mentionsDeleteVerb(envSplitCommandText(words)))
|
|
1379
|
-
return "delete";
|
|
1380
|
-
if (["sh", "bash", "zsh", "dash", "ksh", "ash", "eval", "source", ".", "su", "runuser", "env", "xargs", "find", "busybox", "toybox"].includes(tool ?? "")) {
|
|
1381
|
-
return opaqueOperation(words.join(" "));
|
|
1382
|
-
}
|
|
1383
|
-
if (["mkdir", "mkfile", "touch", "mv", "cp", "ln", "tee", "install", "dd", "rsync", "scp", "truncate"].includes(tool ?? ""))
|
|
1384
|
-
return "write";
|
|
1385
|
-
if (tool === "curl" || tool === "wget")
|
|
1386
|
-
return WRITE_FLAGS.test(words.join(" ")) ? "write" : "read";
|
|
1387
|
-
if (tool === "sed" && words.slice(1).some((word) => /^-i|^--in-place(?:=|$)/.test(word)))
|
|
1388
|
-
return "write";
|
|
1389
|
-
if (["python", "python3", "node", "bun"].includes(tool ?? "") && words.some((word) => word === "-c" || word === "-e"))
|
|
1390
|
-
return "write";
|
|
1391
|
-
return "read";
|
|
1392
|
-
});
|
|
1393
|
-
return operations.includes("delete") ? "delete" : operations.includes("write") ? "write" : "read";
|
|
1394
|
-
}
|
|
1395
|
-
function opaqueOperation(segment) {
|
|
1396
|
-
const trimmed = segment.trim();
|
|
1397
|
-
if (!trimmed)
|
|
1398
|
-
return "read";
|
|
1399
|
-
if (/(?:^|\s)\\?(?:[^\s;&|<>()]*\/)?(?:rm|rmdir|unlink|shred|trash|rmtree|del)(?:\s|$)/.test(trimmed))
|
|
1400
|
-
return "delete";
|
|
1401
|
-
if (/\bgit\b/.test(trimmed)) {
|
|
1402
|
-
if (/\bgit\b[^;&|]*\b(?:clean|rm)\b/.test(trimmed))
|
|
1403
|
-
return "delete";
|
|
1404
|
-
if (/\bgit\b[^;&|]*\b(?:clone|init)\b/.test(trimmed))
|
|
1405
|
-
return "write";
|
|
1406
|
-
return "read";
|
|
1407
|
-
}
|
|
1408
|
-
if (/(?:^|\s)\\?(?:[^\s;&|<>()]*\/)?(?:mkdir|mkfile|touch|mv|cp|ln|tee|install|dd)(?:\s|$)/.test(trimmed))
|
|
1409
|
-
return "write";
|
|
1410
|
-
if (/\b(?:curl|wget)\b/.test(trimmed)) {
|
|
1411
|
-
if (WRITE_FLAGS.test(trimmed))
|
|
1412
|
-
return "write";
|
|
1413
|
-
return "read";
|
|
1414
|
-
}
|
|
1415
|
-
if (/\bsed\b/.test(trimmed)) {
|
|
1416
|
-
if (/(?:^|\s)-i\S*(?:\s|$)|--in-place(?:\s|$)/.test(trimmed))
|
|
1417
|
-
return "write";
|
|
1418
|
-
} else if (/\b(?:rsync|scp|truncate)\b/.test(trimmed)) {
|
|
1419
|
-
return "write";
|
|
1420
|
-
} else if (/(?:^|\s)(?:python3?|node|bun)\b[^;&|]*\s+-[ce](?:\s|$)/.test(trimmed)) {
|
|
1421
|
-
return "write";
|
|
1422
|
-
}
|
|
1423
|
-
if (/(?:^|[;&|])\s*echo\b/.test(trimmed) || />/.test(withoutFdDuplication(trimmed)))
|
|
1424
|
-
return "write";
|
|
1425
|
-
return "read";
|
|
1426
|
-
}
|
|
1427
|
-
function expandHomeSpelling2(token, home) {
|
|
1428
|
-
let body = token.trim();
|
|
1429
|
-
const quoted = body.match(/^(['"])([\s\S]*)\1$/);
|
|
1430
|
-
if (quoted)
|
|
1431
|
-
body = quoted[2];
|
|
1432
|
-
body = body.replace(/^"*\$HOME"*\//, "$HOME/");
|
|
1433
|
-
body = body.replace(/^"*\$\{HOME\}"*\//, "${HOME}/");
|
|
1434
|
-
if (body === "~")
|
|
1435
|
-
return home;
|
|
1436
|
-
if (body.startsWith("~/"))
|
|
1437
|
-
return join3(home, body.slice(2));
|
|
1438
|
-
if (body === "$HOME" || body === "${HOME}")
|
|
1439
|
-
return home;
|
|
1440
|
-
if (body.startsWith("$HOME/"))
|
|
1441
|
-
return join3(home, body.slice("$HOME/".length));
|
|
1442
|
-
if (body.startsWith("${HOME}/"))
|
|
1443
|
-
return join3(home, body.slice("${HOME}/".length));
|
|
1444
|
-
const homeMatch = body.match(new RegExp(`^"*${regexEscape(home)}"*/`));
|
|
1445
|
-
if (homeMatch)
|
|
1446
|
-
return home + "/" + body.slice(homeMatch[0].length);
|
|
1447
|
-
return token;
|
|
1448
|
-
}
|
|
1449
|
-
function unwrapSegment(segment) {
|
|
1450
|
-
let s = segment.trim();
|
|
1451
|
-
while (s.startsWith("(") || s.startsWith("{"))
|
|
1452
|
-
s = s.slice(1).trim();
|
|
1453
|
-
while (s.endsWith(")") || s.endsWith("}"))
|
|
1454
|
-
s = s.slice(0, -1).trim();
|
|
1455
|
-
return s;
|
|
1456
|
-
}
|
|
1457
|
-
function regexEscape(text) {
|
|
1458
|
-
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1459
|
-
}
|
|
1460
|
-
function cdDestinations(operand, bases, home) {
|
|
1461
|
-
if (operand === undefined)
|
|
1462
|
-
return [home];
|
|
1463
|
-
let word = expandHomeSpelling2(operand, home);
|
|
1464
|
-
const quoted = word.match(/^(['"])(.*)\1$/);
|
|
1465
|
-
if (quoted)
|
|
1466
|
-
word = quoted[2];
|
|
1467
|
-
if (!word || word === "-" || /[$\x60*?[\]{}~\\'"]/.test(word))
|
|
1468
|
-
return null;
|
|
1469
|
-
return [...new Set(bases.map((base) => resolve3(base, word)))];
|
|
1470
|
-
}
|
|
1471
|
-
var REL_OPERAND = /(?:^|\s)(\.\.?|[^\s"';&|<>()\x60/]+(?:\/[^\s"';&|<>()\x60]*)?)\s*$/;
|
|
1472
|
-
function bashTargets(command, home, cwd) {
|
|
1473
|
-
if (!command)
|
|
1474
|
-
return [];
|
|
1475
|
-
const roots = protectedRoots(home);
|
|
1476
|
-
const underRoot = (path) => roots.some((root) => path === root || path.startsWith(`${root}${sep2}`));
|
|
1477
|
-
const parts = command.split(/(\s*&&\s*|\s*\|\|\s*|;\s*|\n+)/);
|
|
1478
|
-
const targets = [];
|
|
1479
|
-
let current = [cwd];
|
|
1480
|
-
let reachable = new Set([cwd]);
|
|
1481
|
-
let certain = true;
|
|
1482
|
-
const subshells = [];
|
|
1483
|
-
for (let index = 0;index < parts.length; index += 2) {
|
|
1484
|
-
const separator = index === 0 ? "" : parts[index - 1].trim();
|
|
1485
|
-
if (separator === "||")
|
|
1486
|
-
certain = false;
|
|
1487
|
-
else if (separator !== "&&")
|
|
1488
|
-
certain = true;
|
|
1489
|
-
if (separator !== "&&" || !certain)
|
|
1490
|
-
current = [...reachable];
|
|
1491
|
-
const rawSegment = parts[index];
|
|
1492
|
-
const opens = (rawSegment.match(/^[\s({]*/)?.[0].match(/\(/g) ?? []).length;
|
|
1493
|
-
const closes = (rawSegment.match(/[\s)}]*$/)?.[0].match(/\)/g) ?? []).length;
|
|
1494
|
-
for (let i = 0;i < opens; i++)
|
|
1495
|
-
subshells.push({ current: [...current], reachable: new Set(reachable), certain });
|
|
1496
|
-
const segment = unwrapSegment(rawSegment);
|
|
1497
|
-
const plainCd = segment.match(/^cd(?:\s+(?:-[A-Za-z@]+|--))*(?:\s+([^\s;&|<>()\x60]+))?$/);
|
|
1498
|
-
const embeddedCd = plainCd ? null : segment.match(/(?:^|\s)cd(?:\s+(?:-[A-Za-z]+|--))*\s+([^\s;&|<>()\x60]+)/);
|
|
1499
|
-
if (embeddedCd) {
|
|
1500
|
-
for (const dir of cdDestinations(embeddedCd[1], current, home) ?? [])
|
|
1501
|
-
reachable.add(dir);
|
|
1502
|
-
certain = false;
|
|
1503
|
-
current = [...reachable];
|
|
1504
|
-
}
|
|
1505
|
-
const homeLiteral = regexEscape(home);
|
|
1506
|
-
const suffix = roots.map((root) => regexEscape(root.slice(home.length + 1))).join("|");
|
|
1507
|
-
const prefixRe = new RegExp(`(?:~|\\$HOME"*|\\$\\{HOME\\}"*|${homeLiteral}"*)/(?:${suffix})`);
|
|
1508
|
-
const re = new RegExp(`(${prefixRe.source})([^\\s"';&|<>()\`]*|$)`, "g");
|
|
1509
|
-
const explicitTargets = (text) => {
|
|
1510
|
-
const found = [];
|
|
1511
|
-
let m;
|
|
1512
|
-
re.lastIndex = 0;
|
|
1513
|
-
while ((m = re.exec(text)) !== null)
|
|
1514
|
-
found.push(normalize2(expandHomeSpelling2(m[0], home)).replace(/\/+$/, ""));
|
|
1515
|
-
return found;
|
|
1516
|
-
};
|
|
1517
|
-
const stages = pipelineStages(segment).map((stage) => {
|
|
1518
|
-
const operands = [];
|
|
1519
|
-
let opaqueRedirect = false;
|
|
1520
|
-
const commandPart = withoutFdDuplication(stage).replace(OUTPUT_REDIRECT, (_match, operand) => {
|
|
1521
|
-
if (!operand || /[$\x60]/.test(operand))
|
|
1522
|
-
opaqueRedirect = true;
|
|
1523
|
-
if (operand)
|
|
1524
|
-
operands.push(operand);
|
|
1525
|
-
return " ";
|
|
1526
|
-
});
|
|
1527
|
-
const commandOp = segmentOperation(commandPart);
|
|
1528
|
-
return { commandPart, operands, op: opaqueRedirect && commandOp === "read" ? "write" : commandOp };
|
|
1529
|
-
});
|
|
1530
|
-
const pipelineOp = stages.some((stage) => stage.op === "delete") ? "delete" : stages.some((stage) => stage.op === "write") ? "write" : "read";
|
|
1531
|
-
const bases = current.filter(underRoot);
|
|
1532
|
-
for (const stage of stages) {
|
|
1533
|
-
let foundExplicit = false;
|
|
1534
|
-
for (const path of explicitTargets(stage.commandPart)) {
|
|
1535
|
-
foundExplicit = true;
|
|
1536
|
-
targets.push({ path, op: pipelineOp });
|
|
1537
|
-
}
|
|
1538
|
-
for (const operand of stage.operands) {
|
|
1539
|
-
const explicit = explicitTargets(operand);
|
|
1540
|
-
for (const path of explicit)
|
|
1541
|
-
targets.push({ path, op: "write" });
|
|
1542
|
-
if (explicit.length > 0)
|
|
1543
|
-
continue;
|
|
1544
|
-
for (const base of bases) {
|
|
1545
|
-
targets.push({ path: normalize2(resolve3(base, expandHomeSpelling2(operand, home))), op: "write" });
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
if (foundExplicit || stage.op === "read")
|
|
1549
|
-
continue;
|
|
1550
|
-
const words = shellWords(stage.commandPart);
|
|
1551
|
-
let operandStart = 1;
|
|
1552
|
-
const tool = (words[0]?.text ?? "").split("/").pop();
|
|
1553
|
-
if (tool === "trash" && ["put", "rm", "delete", "remove"].includes(words[1]?.text ?? ""))
|
|
1554
|
-
operandStart = 2;
|
|
1555
|
-
if (tool === "git") {
|
|
1556
|
-
const sub = words.findIndex((word, index2) => index2 > 0 && (word.text === "rm" || word.text === "clean"));
|
|
1557
|
-
if (sub > 0)
|
|
1558
|
-
operandStart = sub + 1;
|
|
1559
|
-
}
|
|
1560
|
-
let endOfOptions = false;
|
|
1561
|
-
const nonFlag = words.slice(operandStart).flatMap((word) => {
|
|
1562
|
-
if (!endOfOptions && word.text === "--") {
|
|
1563
|
-
endOfOptions = true;
|
|
1564
|
-
return [];
|
|
1565
|
-
}
|
|
1566
|
-
if (!endOfOptions && word.text.startsWith("-"))
|
|
1567
|
-
return [];
|
|
1568
|
-
endOfOptions = true;
|
|
1569
|
-
return [word.opaque ? "." : word.text];
|
|
1570
|
-
});
|
|
1571
|
-
const everyOperand = stage.op === "delete" || /(?:^|\s)\\?(?:[^\s;&|<>()]*\/)?(?:mkdir|mkfile|touch|tee|truncate|install|mv)(?:\s|$)/.test(stage.commandPart) || /(?:^|\s)sed(?:\s|$)/.test(stage.commandPart) && /(?:^|\s)(?:-i\S*|--in-place\S*)(?:\s|$)/.test(stage.commandPart);
|
|
1572
|
-
const operandTargets = [];
|
|
1573
|
-
if (everyOperand) {
|
|
1574
|
-
for (const operand of nonFlag.length > 0 ? nonFlag : ["."])
|
|
1575
|
-
operandTargets.push({ operand, op: stage.op });
|
|
1576
|
-
} else {
|
|
1577
|
-
const relMatch = stage.commandPart.match(REL_OPERAND);
|
|
1578
|
-
const last = words.at(-1);
|
|
1579
|
-
operandTargets.push({ operand: relMatch ? relMatch[1] : !last || last.opaque ? "." : last.text, op: stage.op });
|
|
1580
|
-
if (/(?:^|\s)(?:cp|ln|install|mv)(?:\s|$)/.test(stage.commandPart)) {
|
|
1581
|
-
words.forEach((word, index2) => {
|
|
1582
|
-
let value;
|
|
1583
|
-
const short = word.text.match(/^-[A-Za-z]*?t(.*)$/);
|
|
1584
|
-
const long = word.text.match(/^(--t[a-z-]*)(?:=(.*))?$/);
|
|
1585
|
-
if (short && !word.text.startsWith("--")) {
|
|
1586
|
-
value = short[1] ? { text: short[1], opaque: word.opaque } : words[index2 + 1];
|
|
1587
|
-
} else if (long && "--target-directory".startsWith(long[1])) {
|
|
1588
|
-
value = long[2] !== undefined ? { text: long[2], opaque: word.opaque } : words[index2 + 1];
|
|
1589
|
-
}
|
|
1590
|
-
if (value)
|
|
1591
|
-
operandTargets.push({ operand: value.opaque || !value.text ? "." : value.text, op: stage.op });
|
|
1592
|
-
});
|
|
1593
|
-
}
|
|
1594
|
-
if (/(?:^|\s)(?:python3?|node|bun)\b[^;&|]*\s+-[ce](?:\s|$)/.test(stage.commandPart)) {
|
|
1595
|
-
operandTargets.push({ operand: ".", op: stage.op });
|
|
1596
|
-
}
|
|
1597
|
-
}
|
|
1598
|
-
for (const base of bases) {
|
|
1599
|
-
for (const { operand, op } of operandTargets) {
|
|
1600
|
-
targets.push({ path: normalize2(resolve3(base, expandHomeSpelling2(operand, home))), op });
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
if (plainCd) {
|
|
1605
|
-
const next = cdDestinations(plainCd[1], current, home);
|
|
1606
|
-
if (next) {
|
|
1607
|
-
for (const dir of next)
|
|
1608
|
-
reachable.add(dir);
|
|
1609
|
-
if (certain)
|
|
1610
|
-
current = next;
|
|
1611
|
-
} else {
|
|
1612
|
-
certain = false;
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
for (let i = 0;i < closes && subshells.length > 0; i++)
|
|
1616
|
-
({ current, reachable, certain } = subshells.pop());
|
|
1617
|
-
}
|
|
1618
|
-
return targets;
|
|
1619
|
-
}
|
|
1620
|
-
var PATCH_FILE_RE = /^\s*\*\*\*\s+(Add File|Update File|Delete File):\s+(\S+)\s*$/gm;
|
|
1621
|
-
function patchTargets(patch, cwd) {
|
|
1622
|
-
const targets = [];
|
|
1623
|
-
let m;
|
|
1624
|
-
while ((m = PATCH_FILE_RE.exec(patch)) !== null) {
|
|
1625
|
-
const op = m[1] === "Delete File" ? "delete" : "write";
|
|
1626
|
-
targets.push({ path: normalize2(resolve3(cwd, m[2])), op });
|
|
1627
|
-
}
|
|
1628
|
-
return targets;
|
|
1629
|
-
}
|
|
1630
|
-
function evaluate2(input, options = {}) {
|
|
1631
|
-
const warnings = [];
|
|
1632
|
-
if (input.hook_event_name !== "PreToolUse")
|
|
1633
|
-
return { output: { continue: true }, warnings };
|
|
1634
|
-
const tool = typeof input.tool_name === "string" ? input.tool_name : "";
|
|
1635
|
-
const home = options.home ?? homedir();
|
|
1636
|
-
const roots = protectedRoots(home);
|
|
1637
|
-
const orgs = resolveAllowedOrgs(process.env, home);
|
|
1638
|
-
if (tool === "Bash") {
|
|
1639
|
-
const command = getCommand(input);
|
|
1640
|
-
if (!command)
|
|
1641
|
-
return { output: { continue: true }, warnings };
|
|
1642
|
-
const cwd2 = typeof input.cwd === "string" && input.cwd ? input.cwd : process.cwd();
|
|
1643
|
-
const targets = bashTargets(command, home, cwd2);
|
|
1644
|
-
for (const target2 of targets) {
|
|
1645
|
-
for (const root of roots) {
|
|
1646
|
-
const verdict = classifyPath(target2.path, root, orgs, target2.op);
|
|
1647
|
-
if (verdict.blocked) {
|
|
1648
|
-
return { output: { decision: "block", reason: verdict.reason }, warnings };
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
return { output: { continue: true }, warnings };
|
|
1653
|
-
}
|
|
1654
|
-
const toolInput = input.tool_input ?? {};
|
|
1655
|
-
const cwd = typeof input.cwd === "string" && input.cwd ? input.cwd : process.cwd();
|
|
1656
|
-
if (PATCH_TOOLS.has(tool)) {
|
|
1657
|
-
const patch = typeof toolInput.patch === "string" ? toolInput.patch : "";
|
|
1658
|
-
if (!patch)
|
|
1659
|
-
return { output: { continue: true }, warnings };
|
|
1660
|
-
const targets = patchTargets(patch, cwd);
|
|
1661
|
-
for (const target2 of targets) {
|
|
1662
|
-
for (const root of roots) {
|
|
1663
|
-
const verdict = classifyPath(target2.path, root, orgs, target2.op);
|
|
1664
|
-
if (verdict.blocked) {
|
|
1665
|
-
return { output: { decision: "block", reason: verdict.reason }, warnings };
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
return { output: { continue: true }, warnings };
|
|
1670
|
-
}
|
|
1671
|
-
if (!FILE_WRITE_TOOLS.has(tool))
|
|
1672
|
-
return { output: { continue: true }, warnings };
|
|
1673
|
-
const raw = typeof toolInput.file_path === "string" ? toolInput.file_path : typeof toolInput.path === "string" ? toolInput.path : typeof toolInput.notebook_path === "string" ? toolInput.notebook_path : null;
|
|
1674
|
-
if (!raw)
|
|
1675
|
-
return { output: { continue: true }, warnings };
|
|
1676
|
-
const expanded = expandHomeSpelling2(raw, home);
|
|
1677
|
-
const target = isAbsolute3(expanded) ? normalize2(expanded) : resolve3(cwd, expanded);
|
|
1678
|
-
for (const root of roots) {
|
|
1679
|
-
const verdict = classifyPath(target, root, orgs, "write");
|
|
1680
|
-
if (verdict.blocked) {
|
|
1681
|
-
return { output: { decision: "block", reason: verdict.reason }, warnings };
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
return { output: { continue: true }, warnings };
|
|
1685
|
-
}
|
|
1686
|
-
if (false) {}
|
|
1687
|
-
|
|
1688
|
-
// hooks/native-safety-entry.ts
|
|
1689
|
-
var name = process.argv[1];
|
|
1690
|
-
if (name !== "trash-guard" && name !== "workspace-repos-guard")
|
|
1691
|
-
throw new Error("Invalid safety capability");
|
|
1692
|
-
var input = JSON.parse(await Bun.stdin.text());
|
|
1693
|
-
if (!input || Array.isArray(input) || input.hook_event_name !== "PreToolUse" || typeof input.tool_name !== "string" || !input.tool_input || typeof input.tool_input !== "object" || Array.isArray(input.tool_input) || typeof input.cwd !== "string" || !isAbsolute4(input.cwd))
|
|
1694
|
-
throw new Error("Invalid safety input");
|
|
1695
|
-
if (input.tool_name === "Bash" && typeof input.tool_input.command !== "string")
|
|
1696
|
-
throw new Error("Invalid shell input");
|
|
1697
|
-
var requestedExecution = input.hasna_execution;
|
|
1698
|
-
if (requestedExecution !== undefined && (!requestedExecution || Object.keys(requestedExecution).sort().join() !== "agent,schema" || requestedExecution.schema !== "hasna.hooks.execution.v1" || typeof requestedExecution.agent !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:@-]{0,127}$/.test(requestedExecution.agent)))
|
|
1699
|
-
throw new Error("Invalid execution capability");
|
|
1700
|
-
var execution = requestedExecution === undefined ? undefined : {
|
|
1701
|
-
runtime: process.execPath,
|
|
1702
|
-
home: homedir2(),
|
|
1703
|
-
path: process.env.PATH,
|
|
1704
|
-
agent: requestedExecution.agent
|
|
1705
|
-
};
|
|
1706
|
-
if (execution && (!isAbsolute4(execution.runtime) || !execution.path))
|
|
1707
|
-
throw new Error("Invalid bound execution context");
|
|
1708
|
-
if (["apply_patch", "ApplyPatch", "functions.apply_patch"].includes(input.tool_name)) {
|
|
1709
|
-
if (input.tool_input.command !== undefined && input.tool_input.patch !== undefined && input.tool_input.command !== input.tool_input.patch)
|
|
1710
|
-
throw new Error("Conflicting patch aliases");
|
|
1711
|
-
const patch = input.tool_input.command ?? input.tool_input.patch;
|
|
1712
|
-
if (typeof patch !== "string")
|
|
1713
|
-
throw new Error("Invalid patch input");
|
|
1714
|
-
input.tool_input = { ...input.tool_input, command: patch, patch };
|
|
1715
|
-
}
|
|
1716
|
-
var repos = evaluate2(input).output;
|
|
1717
|
-
var handoff = input.tool_name === "Bash" && scanCommand(input.tool_input.command, { home: homedir2(), cwd: input.cwd }).handoffHits.length > 0;
|
|
1718
|
-
var verdict = repos.decision === "block" ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: repos.reason } } : handoff ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: "[workspace-repos-guard] Deletion under a protected repository checkout is refused." } } : name === "trash-guard" ? evaluate(input, { home: homedir2(), cwd: input.cwd, findTrash: findTrashBinary, inspectTrash: inspectTrashBinary, execution }) : { continue: true };
|
|
1719
|
-
process.stdout.write(JSON.stringify({ verdict: "continue" in verdict ? null : verdict }));
|
|
2
|
+
import{homedir as T0}from"os";import{isAbsolute as B1}from"path";import{isAbsolute as A1,join as ZZ,normalize as L1,resolve as P1,sep as pZ}from"path";import{dirname as t,isAbsolute as PZ,join as lZ,resolve as s}from"path";import{accessSync as M1,closeSync as F1,constants as AZ,fstatSync as N1,lstatSync as HZ,openSync as q1,readFileSync as _1,readlinkSync as j1,readSync as W1,realpathSync as mZ,statSync as cZ}from"fs";import{spawnSync as T1}from"child_process";var A0={missing:"No verified @hasna/trash guard was found on PATH.",unsafe_executable:"The Trash candidate is not an owned executable file or is writable by group or others.",unsafe_manifest:"The Trash package manifest is not a safe owned regular file.",unsafe_directory:"A Trash PATH or package directory is not owned safely or is writable by group or others.",unsafe_interpreter:"The Trash interpreter is missing, unsupported, or not a safe owned executable on its actual PATH.",unrecognized_package:"The candidate is not the registered @hasna/trash executable.",identity_failed:"The installed Trash identity probe failed or exceeded its deadline.",unsupported_protocol:"The installed Trash version does not expose the required guard protocol."};function BZ(Z){return Z===0||Z===process.getuid?.()}function LZ(Z,J=!1){let $=Z;for(let X=0;X<32;X++){if(!PZ($)||$.length>4096||!UZ("/"))return!1;let z=$.split("/"),Q=z.pop();if(!Q||Q==="."||Q==="..")return!1;let K="/";for(let H of z){if(!H||H===".")continue;if(H===".."){K=t(K);continue}let B=lZ(K,H),U=HZ(B);if(J&&B==="/bin"&&U.isSymbolicLink()&&U.uid===0){let V=mZ(B);if(!UZ(V))return!1;K=V;continue}if(!U.isDirectory()||!BZ(U.uid)||U.mode&18)return!1;K=B}let G=lZ(K,Q),Y=HZ(G);if(!BZ(Y.uid))return!1;if(Y.isSymbolicLink()){let H=j1(G);$=PZ(H)?H:K+"/"+H;continue}return Y.isFile()&&Boolean(Y.mode&73)&&!(Y.mode&18)}return!1}function C1(Z){if(!PZ(Z)||s(Z)!==(Z.replace(/\/+$/,"")||"/"))return!1;for(let J=s(Z);;J=t(J)){try{return HZ(J),UZ(J)}catch($){if($.code!=="ENOENT")return!1}if(t(J)===J)return!1}}function I1(Z,J){let $=q1(Z,AZ.O_RDONLY|AZ.O_NONBLOCK|AZ.O_NOFOLLOW),X;try{if(!N1($).isFile())return!1;let Q=Buffer.alloc(256),K=W1($,Q,0,Q.length,0);X=Q.subarray(0,K).toString("utf8").split(`
|
|
3
|
+
`)[0]}finally{F1($)}let z=/^#!(\/[A-Za-z0-9_./-]+)$/.exec(X);if(z&&z[1]!=="/usr/bin/env")return LZ(z[1],!0);if(X!=="#!/usr/bin/env bun"||!LZ("/usr/bin/env",!0))return!1;for(let Q of J.split(":").slice(0,64)){if(!C1(Q))return!1;let K=s(Q,"bun");try{if(!cZ(K).isFile())continue;M1(K,AZ.X_OK)}catch{continue}return LZ(K,!0)}return!1}function UZ(Z){let J=Z;for(;;){let $=HZ(J);if(!$.isDirectory()||!BZ($.uid)||($.mode&18)!==0)return!1;let X=t(J);if(X===J)return!0;J=X}}function VZ(Z=process.env){let J=Z.PATH??"",$=Date.now()+2000,X="missing",z=(Q)=>{if(X==="missing"||X==="unrecognized_package"||Q.startsWith("unsafe_"))X=Q};for(let Q of J.split(":").slice(0,64)){if(!PZ(Q)||Date.now()>=$)continue;let K=s(Q,"trash");try{let G=cZ(K);if(!G.isFile()||!BZ(G.uid)||(G.mode&73)===0||(G.mode&18)!==0){z("unsafe_executable");continue}let Y=mZ(K);if(!UZ(s(Q))||!UZ(t(Y))){z("unsafe_directory");continue}if(!LZ(K)){z("unsafe_directory");continue}let H=s(t(Y),"../.."),B=lZ(H,"package.json"),U=cZ(B);if(!U.isFile()||!BZ(U.uid)||U.size>16384||(U.mode&18)!==0||HZ(B).isSymbolicLink()){z("unsafe_manifest");continue}let V=JSON.parse(_1(B,"utf8"));if(V.name!=="@hasna/trash"||typeof V.version!=="string"||typeof V.bin?.trash!=="string"||mZ(s(H,V.bin.trash))!==Y){z("unrecognized_package");continue}try{if(!I1(Y,J)){z("unsafe_interpreter");continue}}catch{z("unsafe_interpreter");continue}let I=$-Date.now();if(I<=0){z("identity_failed");break}let j=T1(K,["--identity"],{encoding:"utf8",timeout:I,maxBuffer:2048,env:{PATH:J},stdio:["ignore","pipe","pipe"]});if(j.error||j.status!==0||Date.now()>=$){z("identity_failed");continue}let P=JSON.parse(j.stdout);if(P.name==="@hasna/trash"&&P.version===V.version&&P.guardProtocol==="hasna.trash.guard.v1")return{ok:!0,path:K,version:V.version};z("unsupported_protocol")}catch{}}return{ok:!1,code:X}}function OZ(Z=process.env){let J=VZ(Z);return J.ok?J.path:null}function e(Z){let J=Z.tool_input?.command;return typeof J==="string"?J:""}var L0=["/","/bin","/boot","/dev","/etc","/home","/lib","/lib32","/lib64","/libx32","/opt","/proc","/root","/run","/sbin","/srv","/sys","/usr","/var","/Applications","/Library","/System","/Users","/Volumes","/private"];var O1=600000,E1="Delete via trash guard (rm intercepted and made recoverable)",R1=[".hasna/repos/clones","workspace/repos"],y1=[".hasna",".ssh",".aws"],P0={rmdir:"`rmdir` is a delete that trash-guard does not rewrite (it takes rmdir's flags, not rm's). Re-run it as `rm -d -- <path>` for an empty directory, or `rm -r -- <path>` for a non-empty one, and it is redirected into trash.",unlink:"`unlink` is a delete that trash-guard does not rewrite. Re-run it as `rm -- <path>` and it is redirected into trash.",shred:"`shred` destroys content irrecoverably and is never allowed. Re-run it as `rm -- <path>` and it is redirected into trash."},v1=new Set(["sh","bash","zsh","dash","ksh","ash","eval","source",".","su","runuser","env"]),S1={sudo:["-u","--user","-g","--group","-p","--prompt","-C","--chdir","-h","--host","-r","--role","-t","--type","-U","--other-user"],doas:["-u","-C"],env:["-u","--unset","-C","--chdir","-S","--split-string"],nice:["-n","--adjustment"],ionice:["-c","--class","-n","--classdata","-p","--pid"],stdbuf:["-i","-o","-e","--input","--output","--error"],time:["-f","--format","-o","--output"],timeout:["-s","--signal","-k","--kill-after"],nohup:[],setsid:[],command:[],builtin:[],exec:["-a"]},b1={timeout:1},f1=new Set(["busybox","toybox"]),k1=new Set(["!","{","}","then","do","else","elif","if","while","until","for","case","esac","fi","done","select","function"]),w1=new Set(["-C","-c","--git-dir","--work-tree","--namespace","--exec-path","--config-env"]),x1=/(^|[^A-Za-z0-9_.-])(rm|rmdir|unlink|shred)(?![A-Za-z0-9_.-])/;function FZ(Z){return x1.test(A(Z))}function qZ(Z){return Z.map((J)=>J.replace(/^-[iv0]*S|^--split-string=/," ")).join(" ")}var y0=/^[A-Za-z_][A-Za-z0-9_]*=/;function NZ(Z){let J=Z.lastIndexOf("/");return J>=0?Z.slice(J+1):Z}function v0(Z,J){let $=Z.indexOf("'",J+1);return $===-1?-1:$+1}function iZ(Z,J){let $=J+1;while($<Z.length){if(Z[$]==="\\"){$+=2;continue}if(Z[$]==="`")return $+1;$++}return-1}function S0(Z,J){let $=J+1;while($<Z.length){let X=Z[$];if(X==="\\"){$+=2;continue}if(X==='"')return $+1;if(X==="`"){let z=iZ(Z,$);if(z<0)return-1;$=z;continue}if(X==="$"&&Z[$+1]==="("){let z=_Z(Z,$+1);if(z<0)return-1;$=z;continue}$++}return-1}function EZ(Z,J){if(Z[J]!=="<"||Z[J+1]!=="<"||Z[J+2]==="<")return null;let $=J+2,X=Z[$]==="-";if(X)$++;while(Z[$]===" "||Z[$]==="\t")$++;let z="",Q=!1;while($<Z.length&&!/[\s;|&<>()]/.test(Z[$])){let K=Z[$];if(K==="'"||K==='"'){let G=Z.indexOf(K,$+1);if(G<0)return null;z+=Z.slice($+1,G),Q=!0,$=G+1;continue}if(K==="\\"){if($+1>=Z.length)return null;z+=Z[$+1],Q=!0,$+=2;continue}z+=K,$++}return z?{delimiter:z,stripTabs:X,quoted:Q,end:$}:null}function RZ(Z,J,$){let X=J+1;for(let z of $)for(;;){if(X>Z.length)return-1;let Q=Z.indexOf(`
|
|
4
|
+
`,X),K=Q===-1?Z.length:Q,G=Z.slice(X,K);if(G.endsWith("\r"))G=G.slice(0,-1);let Y=z.stripTabs?G.length-G.replace(/^\t+/,"").length:0;if(z.stripTabs)G=G.slice(Y);if(G===z.delimiter){X=K+1;break}if(G.startsWith(z.delimiter)&&/^\s*\)/.test(G.slice(z.delimiter.length))){if(z===$[$.length-1])return X+Y+z.delimiter.length;return-1}if(Q===-1)return-1;X=Q+1}return X-1}function _Z(Z,J){let $=Z.length,X=Z[J+1]==="(",z=0,Q=J,K=[];while(Q<$){let G=Z[Q];if(G==="\\"){Q+=2;continue}if(G==="'"){let Y=v0(Z,Q);if(Y<0)return-1;Q=Y;continue}if(G==='"'){let Y=S0(Z,Q);if(Y<0)return-1;Q=Y;continue}if(G==="`"){let Y=iZ(Z,Q);if(Y<0)return-1;Q=Y;continue}if(!X&&G==="<"&&Z[Q+1]==="<"){if(Z[Q+2]==="<"){Q+=3;continue}let Y=EZ(Z,Q);if(!Y)return-1;K.push(Y),Q=Y.end;continue}if(G===`
|
|
5
|
+
`&&K.length>0){let Y=RZ(Z,Q,K);if(Y<0)return-1;K=[],Q=Y;continue}if(!X&&G==="#"&&/[\s;&|(]/.test(Z[Q-1])){let Y=Z.indexOf(`
|
|
6
|
+
`,Q);if(Y<0)return-1;Q=Y;continue}if(G==="(")z++;else if(G===")"){if(z--,z===0)return K.length>0?-1:Q+1}Q++}return-1}function $Z(Z){let J=[],$=[],X=[],z=new Set,Q=-1,K=!1,G=!0,Y=0,H=Z.length,B=(M)=>{if(Q>=0){let _=X.length;if(X.push({text:Z.slice(Q,M),start:Q,end:M}),K)z.add(_);Q=-1,K=!1}},U=(M)=>{if(B(M),X.length>0)J.push({words:X,redirects:z});X=[],z=new Set,K=!1},V=(M)=>v0(Z,M),I=(M)=>iZ(Z,M),j=(M)=>_Z(Z,M),P=(M)=>S0(Z,M);while(Y<H){let M=Z[Y];if(M===" "||M==="\t"||M==="\r"){B(Y),Y++;continue}if(M===`
|
|
7
|
+
`){U(Y),Y++;continue}if(M===";"){U(Y),Y++;continue}if(M==="&"){U(Y),Y+=Z[Y+1]==="&"?2:1;continue}if(M==="|"){U(Y),Y+=Z[Y+1]==="|"?2:1;continue}if(M==="("||M===")"){U(Y),Y++;continue}if(M==="<"&&Z[Y+1]==="<"&&Z[Y+2]!=="<"){let _=Z.slice(Y+2).match(/^-?[ \t]*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1/);if(!_){G=!1,Y=H;continue}let g=Z.indexOf(`
|
|
8
|
+
`,Y+2+_[0].length);if(g===-1){G=!1,Y=H;continue}let h=Z.slice(g+1),d=g1(h,_[2]);if(d===-1){G=!1,Y=H;continue}B(Y),Y=g+1+d+_[2].length;continue}if(M===">"||M==="<"){if(Q>=0&&/^\d+$/.test(Z.slice(Q,Y)))Q=-1;if(B(Y),K=!0,Y++,Z[Y]===M||Z[Y]==="&"||M===">"&&Z[Y]==="|"||M==="<"&&Z[Y]===">")Y++;continue}if(M==="'"){if(Q<0)Q=Y;let _=V(Y);if(_<0)G=!1,Y=H;else Y=_;continue}if(M==='"'){if(Q<0)Q=Y;let _=P(Y);if(_<0)G=!1,Y=H;else Y=_;continue}if(M==="`"){if(Q<0)Q=Y;let _=I(Y);if(_<0)G=!1,Y=H;else $.push(Z.slice(Y+1,_-1)),Y=_;continue}if(M==="$"&&Z[Y+1]==="("){if(Q<0)Q=Y;let _=j(Y+1);if(_<0)G=!1,Y=H;else $.push(Z.slice(Y+2,_-1)),Y=_;continue}if(M==="\\"){if(Q<0)Q=Y;if(Y+1>=H)G=!1,Y=H;else Y+=2;continue}if(Q<0)Q=Y;Y++}return U(H),{segments:J,trustworthy:G,substitutions:$}}function g1(Z,J){let $=0;while($<=Z.length){let X=Z.indexOf(J,$);if(X===-1)return-1;let z=X===0||Z[X-1]===`
|
|
9
|
+
`,Q=X+J.length,K=Q===Z.length||Z[Q]===`
|
|
10
|
+
`||Z[Q]==="\r";if(z&&K)return X;$=X+1}return-1}function A(Z){let J="",$=0;while($<Z.length){let X=Z[$];if(X==="\\"){if($+1<Z.length)J+=Z[$+1];$+=2;continue}if(X==="'"){let z=Z.indexOf("'",$+1);if(z===-1){J+=Z.slice($+1);break}J+=Z.slice($+1,z),$=z+1;continue}if(X==='"'){$++;while($<Z.length&&Z[$]!=='"'){if(Z[$]==="\\"&&$+1<Z.length){J+=Z[$+1],$+=2;continue}J+=Z[$],$++}$++;continue}J+=X,$++}return J}function u(Z){let J=0,$=0;while(J<Z.words.length&&$++<32){if(Z.redirects.has(J)){J++;continue}let X=Z.words[J].text,z=A(X);if(y0.test(z)&&!z.startsWith("-")){J++;continue}if(k1.has(z)){J++;continue}let Q=NZ(z);if(Q==="command")for(let G of Z.words.filter((Y,H)=>H>J&&!Z.redirects.has(H))){let Y=A(G.text);if(Y==="--"||!Y.startsWith("-"))break;if(/^-[pvV]*[vV][pvV]*$/.test(Y))return-1}let K=S1[Q];if(K){let G=J;J++;while(J<Z.words.length){if(Z.redirects.has(J)){J++;continue}let H=A(Z.words[J].text);if(!H.startsWith("-")||H==="-")break;if(H==="--"){J++;break}if(Q==="env"&&(/^-[iv0]*S/.test(H)||/^--split-string(?:=|$)/.test(H)))return G;if(J++,K.includes(H)||Q==="exec"&&/^-[cl]*a$/.test(H)){while(Z.redirects.has(J))J++;J++}}let Y=b1[Q]??0;while(Y>0&&J<Z.words.length){let H=A(Z.words[J].text);if(H.startsWith("-")&&H!=="-")break;J++,Y--}continue}return J}return-1}function h1(Z,J){let $=[],X=!1;for(let z=J+1;z<Z.words.length;z++){if(Z.redirects.has(z))continue;let Q=A(Z.words[z].text);if(!X){if(Q==="--"){X=!0;continue}if(Q.startsWith("-")&&Q!=="-")continue}$.push(Z.words[z])}return $}function u1(Z){return R1.map((J)=>ZZ(Z,...J.split("/")))}function l1(Z,J){if(Z==="~"||Z==="$HOME"||Z==="${HOME}")return J;if(Z.startsWith("~/"))return ZZ(J,Z.slice(2));if(Z.startsWith("$HOME/"))return ZZ(J,Z.slice(6));if(Z.startsWith("${HOME}/"))return ZZ(J,Z.slice(8));return Z}function dZ(Z,J,$){let X=A(Z);if(!X)return null;if(X.includes("`"))return null;if(X==="$HOME"||X==="${HOME}"||X==="$HOME/"||X==="${HOME}/")return $;if(X.replace(/\$\{HOME\}/g,"").replace(/\$HOME/g,"").includes("$"))return null;if(/[*?[\]]/.test(X))return null;if(X.startsWith("~")){let G=l1(X,$);if(G===X)return null;X=G}let K=(A1(X)?L1(X):P1(J,X)).replace(/\/+$/,"");return K===""?"/":K}function MZ(Z,J){return Z===J||Z.startsWith(`${J}${pZ}`)}function m1(Z,J){let $=`${ZZ(J,".hasna","scratchpad","scratch")}${pZ}`;if(!Z.startsWith($))return!1;let[X,z]=Z.slice($.length).split(pZ);if(!X||!/^[a-z0-9][a-z0-9._-]{0,63}$/.test(X)||X.endsWith(".")||!z)return!1;let Q=z.normalize("NFKC").toLowerCase();return Q!=="meta.json"&&Q!==".lock"}function c1(Z,J){if(Z===J)return`\`${Z}\` is the home directory itself, which is never deleted or trashed. Delete a specific file or directory inside it instead.`;for(let $ of L0){if(Z!==$&&!MZ($,Z))continue;return $==="/"?`\`${Z}\` is the filesystem root, which is never deleted.`:`\`${Z}\` is the system root \`${$}\` (or an ancestor of it), which is never deleted or trashed.`}for(let $ of y1){let X=ZZ(J,$);if($===".hasna"&&m1(Z,J))continue;if(!MZ(Z,X)&&!MZ(X,Z))continue;return`\`${Z}\` is the protected \`~/${$}\` path (or contains it), which is never deleted or trashed. Delete a specific file inside it, and only when you are sure.`}return null}function p1(Z,J,$){let X=u1($);if(X.some((z)=>MZ(J,z)))return!0;for(let z of Z){let Q=dZ(z.text,J,$);if(Q&&X.some((K)=>MZ(Q,K)))return!0}return!1}function jZ(Z,J){let $={cwd:J.cwd,home:J.home,depth:J.depth??0},X={rmHits:[],handoffHits:[],refusals:[],protectedHits:[],trustworthy:!0},z=$Z(Z);if(X.trustworthy=z.trustworthy,!z.trustworthy)return X;let Q=$.cwd;for(let K of z.segments){let G=u(K);if(G<0)continue;let Y=NZ(A(K.words[G].text));if(Y==="cd"){let U=K.words[G+1];if(U){let V=dZ(U.text,Q,$.home);if(V)Q=V;else Q=$.cwd}continue}let H=P0[Y];if(H){X.refusals.push({reason:H});continue}if(Y==="rm"){let U=h1(K,G);if(U.length===0)continue;let V={word:K.words[G],operands:U};if(p1(V.operands,Q,$.home)){X.handoffHits.push(V);continue}X.rmHits.push(V);for(let I of V.operands){let j=dZ(I.text,Q,$.home);if(!j)continue;let P=c1(j,$.home);if(P){X.protectedHits.push({reason:P});break}}continue}if(f1.has(Y)){let U=K.words.findIndex((I,j)=>j>G&&!A(I.text).startsWith("-")),V=U>=0?NZ(A(K.words[U].text)):"";if(V==="rm"||V in P0)X.refusals.push({reason:`\`${Y} ${V}\` runs the ${Y} applet, whose own \`rm\` is not GNU \`rm\` and cannot be rewritten in place. Run the delete as \`rm -- <path>\`, which is redirected into trash.`});continue}if(Y==="git"){let U=sZ(K,G);if(U==="rm"||U==="clean"){if(!(U==="rm"?i1(K,G):b0(K,G)))X.refusals.push({reason:U==="rm"?"`git rm` is refused: without `--cached` it deletes the working-tree file, and its semantics are not `rm`'s. Use `rm -- <path>` (intercepted and redirected into trash) to remove the file, or `git rm --cached -- <path>` to unstage it without deleting.":"`git clean` deletes untracked files irrecoverably and is refused. Remove them explicitly with `rm -- <path>` (intercepted and redirected into trash), or preview with `git clean -n`."})}continue}if(Y==="find"){let U=K.words.map((j)=>A(j.text));if(U.indexOf("-delete")>=0){X.refusals.push({reason:"`find -delete` deletes files irrecoverably and is refused. List the paths first (`find \u2026 -print`), then remove them explicitly with `rm -- <path>` so the delete is redirected into trash."});continue}let I=U.findIndex((j)=>j==="-exec"||j==="-execdir"||j==="-ok");if(I>=0&&U.slice(I).some((j)=>j==="rm"||j==="rmdir"||j==="unlink"||j==="shred")){X.refusals.push({reason:"`find -exec <delete>` is refused: the delete runs through find, not through `rm`, and cannot be redirected into trash. Remove the paths explicitly with `rm -- <path>`."});continue}continue}if(Y==="xargs"&&K.words.some((U)=>A(U.text)==="rm")){X.refusals.push({reason:"`xargs rm` takes its targets from stdin, so this hook cannot verify what would be deleted. Pipe the paths into an explicit `rm -- <path> \u2026` instead, which is redirected into trash."});continue}let B=Y==="env"?qZ(K.words.map((U)=>A(U.text))):d1(K);if(v1.has(Y)&&FZ(B)){X.refusals.push({reason:`\`${Y}\` runs a command string this hook cannot rewrite in place. Run the delete directly as \`rm -- <path>\` (intercepted and redirected into trash), or as a script that uses \`trash guard\` itself.`});continue}}if($.depth<3)for(let K of z.substitutions){let G=jZ(K,{cwd:Q,home:$.home,depth:$.depth+1});if(G.rmHits.length>0||G.refusals.length>0||G.handoffHits.length>0||G.protectedHits.length>0){X.refusals.push({reason:"This command runs a delete inside a command substitution `$(\u2026)`, which this hook cannot rewrite. Run the delete as a separate `rm -- <path>` command, which is redirected into trash."});break}}else if(z.substitutions.some((K)=>FZ(K)))X.refusals.push({reason:"This command nests a delete inside command substitutions deeper than this hook will follow. Run the delete as a separate `rm -- <path>` command."});return X}function d1(Z){return Z.words.map((J)=>J.text).join(" ")}function sZ(Z,J){let $=J+1;while($<Z.words.length){let X=A(Z.words[$].text);if(!X.startsWith("-"))return X;if($++,w1.has(X))$++}return null}function i1(Z,J){if(Z.words.slice(J+1).map((X)=>A(X.text)).includes("--cached"))return!0;return b0(Z,J)}function b0(Z,J){let $=Z.words.slice(J+1).map((X)=>A(X.text));return $.includes("-n")||$.includes("--dry-run")}function O0(Z){if(/^[A-Za-z0-9_\-./+=:,@%^]+$/.test(Z))return Z;return`'${Z.replace(/'/g,"'\\''")}'`}function s1(Z,J,$,X){let z=X?["/usr/bin/env","-i",`HOME=${X.home}`,`PATH=${X.path}`,`HASNA_TRASH_AGENT=${X.agent}`,X.runtime,"--config=/dev/null","--no-env-file","--no-macros","--no-install",$,"guard"].map(O0).join(" "):`${O0($)} guard`,Q=[...J].sort((G,Y)=>Y.word.start-G.word.start),K=Z;for(let G of Q)K=`${K.slice(0,G.word.start)}${z}${K.slice(G.word.end)}`;return K}function a1(Z,J,$,X,z){if(!J||J===Z)return!1;if($.length===0)return!1;let Q=jZ(J,{cwd:X,home:z});if(!Q.trustworthy)return!1;if(Q.rmHits.length>0||Q.handoffHits.length>0)return!1;return Q.refusals.length===0}function n1(Z,J){let $={};for(let[X,z]of Object.entries(Z??{}))$[X]=z;if($.command=J,typeof $.description!=="string"||$.description.trim()==="")$.description=E1;if(typeof $.timeout!=="number"||!Number.isFinite($.timeout)||$.timeout<=0)$.timeout=O1;if(typeof $.run_in_background!=="boolean")$.run_in_background=!1;if("dangerouslyDisableSandbox"in $&&typeof $.dangerouslyDisableSandbox!=="boolean")delete $.dangerouslyDisableSandbox;return $}function r1(Z,J,$,X,z){if(typeof Z.command!=="string"||Z.command.trim()==="")return!1;if(typeof Z.description!=="string"||Z.description.trim()==="")return!1;if(typeof Z.timeout!=="number"||!Number.isFinite(Z.timeout)||Z.timeout<=0)return!1;if(typeof Z.run_in_background!=="boolean")return!1;if("dangerouslyDisableSandbox"in Z&&typeof Z.dangerouslyDisableSandbox!=="boolean")return!1;return a1(J,Z.command,$,X,z)}function o1(Z){let J=new Set(["export","set","unset","readonly","declare","typeset","local","alias","unalias","read","readarray","mapfile","getopts","trap","enable","function","env","sudo","doas","stdbuf"]);return $Z(Z).segments.some((X)=>{let z=u(X);if(X.words.slice(0,z<0?X.words.length:z).some((G)=>y0.test(A(G.text))||J.has(NZ(A(G.text)))))return!0;if(z<0)return!1;let K=X.words[z];return J.has(NZ(A(K.text)))||/^\s*\(\s*\)\s*\{/.test(Z.slice(K.end))})}function k(Z){return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:Z}}}function t1(Z){return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",updatedInput:Z}}}function e1(Z){try{if(Z.inspectTrash){let J=Z.inspectTrash();return J.ok?{path:J.path,error:null}:{path:null,error:`${J.code}: ${A0[J.code]}`}}return{path:Z.findTrash(),error:null}}catch(J){return{path:null,error:J instanceof Error?J.message:String(J)}}}var Z2="[trash-guard] No verified @hasna/trash guard was found on PATH. This deletion is refused. Install the current @hasna/trash package with Bun and run its setup; the operating-system trash utility is not a compatible guard.";function f0(Z,J){if(Z.hook_event_name!=="PreToolUse")return{continue:!0};if(["apply_patch","ApplyPatch","functions.apply_patch"].includes(Z.tool_name??"")){let H=Z.tool_input?.command;if(typeof H!=="string")return k("[trash-guard] Unreadable patch input; deletion safety cannot be checked.");if(/^\s*\*\*\* Delete File:/m.test(H))return k("[trash-guard] Delete File would bypass recoverable deletion. First use `trash put -- <path>` or the trash_put MCP tool, then submit any remaining edits without the deletion block.");return{continue:!0}}if(Z.tool_name!=="Bash")return{continue:!0};if(typeof Z.tool_input?.command!=="string")return k("[trash-guard] The Bash tool call carried no readable `command`, so this hook cannot verify what would run. Refusing a call it cannot verify. Re-run the delete explicitly as `rm -- <path>` so it can be redirected into trash.");let X=e(Z);if(!X.trim())return{continue:!0};let z=jZ(X,{cwd:J.cwd,home:J.home});if(z.refusals.length>0)return k(`[trash-guard] ${z.refusals[0].reason}`);if(z.protectedHits.length>0)return k(`[trash-guard] ${z.protectedHits[0].reason}`);if(!z.trustworthy)return FZ(X)?k("[trash-guard] This command could not be parsed to the end (unterminated quote, substitution or here-document), so a delete inside it cannot be redirected. Re-run the delete as a plain `rm -- <path>` command."):{continue:!0};if(z.rmHits.length===0){if(z.handoffHits.length>0)return{continue:!0};return{continue:!0}}if(z.handoffHits.length>0)return k("[trash-guard] This command mixes a delete under the protected repo-checkout roots (owned by workspace-repos-guard) with a delete outside them, and a partial rewrite would leave one of them unredirected. Re-run them as separate commands, so each delete is handled by the hook that owns it.");let Q=e1(J);if(Q.error!==null)return k(`[trash-guard] The guard could not resolve the \`trash\` binary (${Q.error}), so this \`rm\` cannot be redirected into a recoverable delete \u2014 and an unrecoverable delete is never allowed, so the command is refused rather than run. Fix the environment (PATH, permissions) and re-run the same command.`);if(Q.path===null)return k(Z2);let K=Q.path;if(J.execution&&o1(X))return k("[trash-guard] A deletion combined with shell environment changes cannot use the verified native execution context. Run the deletion as a separate plain rm command.");let G=s1(X,z.rmHits,K,J.execution),Y=n1(Z.tool_input,G);if(J.execution)Y.hasna_execution="hasna.hooks.execution.v1";if(!r1(Y,X,z.rmHits,J.cwd,J.home))return k("[trash-guard] The rewrite of this command did not verify (the delete could not be redirected cleanly), so the command is refused rather than run unredirected. Re-run the delete as a plain `rm -- <path>` command.");return t1(Y)}import{closeSync as $2,constants as k0,fstatSync as J2,lstatSync as X2,openSync as z2,readFileSync as Y2,statSync as Q2}from"fs";import{homedir as g0}from"os";import{dirname as aZ,isAbsolute as h0,join as JZ,normalize as XZ,relative as K2,resolve as CZ,sep as oZ}from"path";var G2=new Set(["Write","Edit","MultiEdit","NotebookEdit"]),H2=new Set(["apply_patch","ApplyPatch","functions.apply_patch"]),B2=["hasna","hasnaxyz","hasna-products"],WZ="workspace-repos-guard";function U2(Z){return JZ(Z,".hasna","repos","clones")}function V2(Z){return JZ(Z,"workspace","repos")}function u0(Z){return[U2(Z),V2(Z)]}function M2(Z){return JZ(Z,".hasna","hooks","config","workspace-repos-guard-orgs")}var F2=/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;function nZ(Z){let J=process.getuid?.();return J!==void 0&&(Z.uid===0||Z.uid===J)&&(Z.mode&18)===0}function N2(Z){let J=M2(Z),$;try{let z=z2(J,k0.O_RDONLY|k0.O_NOFOLLOW);try{let K=J2(z);if(!K.isFile()||!nZ(K)||K.size>4096)return null;$=Y2(z,"utf8")}finally{$2(z)}let Q=XZ(Z);for(let K=aZ(J);K!==Q;K=aZ(K)){let G=X2(K);if(!G.isDirectory()||!nZ(G)||K===aZ(K))return null}if(!nZ(Q2(Q)))return null}catch{return null}let X=$.replace(/#[^\n]*/g,"").split(/[\s,]+/).filter(Boolean);return X.length>0&&X.every((z)=>F2.test(z))?new Set(X):null}function q2(Z=process.env,J=g0()){let $=Z.WORKSPACE_REPOS_GUARD_ORGS;if(typeof $==="string"&&$.trim())return new Set($.split(",").map((X)=>X.trim()).filter(Boolean));return N2(J)??new Set(B2)}function rZ(Z,J,$,X){if(X==="read")return{blocked:!1};let z=K2(J,Z);if(z===""){if(X==="delete")return{blocked:!0,reason:`[${WZ}] delete of the protected repo-checkout root is forbidden: ${J}`};return{blocked:!0,reason:`[${WZ}] writes directly to the protected repo-checkout root are forbidden: ${J}`}}if(z.startsWith(`..${oZ}`)||z===".."||h0(z))return{blocked:!1};let Q=z.split(oZ).filter(Boolean);if(X==="delete")return{blocked:!0,reason:`[${WZ}] delete under the protected repo-checkout root (${J}) is forbidden at any depth: ${Z}`};if(Q.length<=1)return{blocked:!0,reason:`[${WZ}] writing would create a top-level entry directly under the protected repo-checkout root (${J}); only GitHub-org folders belong there: ${Z}`};let K=Q[0];if(!$.has(K))return{blocked:!0,reason:`[${WZ}] '${K}' is not an allowed GitHub org under the protected repo-checkout root (${J}); allowed: ${[...$].join(", ")}: ${Z}`};return{blocked:!1}}var l0=/\b(?:-o|--output|-O|--output-document|-out)\b/,_2=/\d*[<>]&\s*(?:\d+|-)(?=$|[\s;&|)])/g,j2=/(?:\d+|&)?(?:>>|>\||>&|<>|>)\s*([^\s;&|<>()\x60]*)/g;function m0(Z){return Z.replace(_2," ")}function W2(Z){let J=[],$=null,X=0,z=0,Q=!1,K=0;for(let G=0;G<Z.length;G++){let Y=Z[G];if($==="'"){if(Y==="'")$=null;continue}if(Y==="\\"){G++;continue}if(Y==="("){X++;continue}if(Y===")"){if(--X<0)return[Z];continue}if(Y==="$"&&Z[G+1]==="{"){z++,G++;continue}if(Y==="}"&&z>0){z--;continue}if(Y==="`"){Q=!Q;continue}if($==='"'){if(Y==='"')$=null;continue}if(Y==="'"||Y==='"'){$=Y;continue}if(Y!=="|"||X>0||z>0||Q||Z[G-1]===">")continue;if(J.push(Z.slice(K,G)),Z[G+1]==="&")G++;K=G+1}if($||X!==0||z!==0||Q)return[Z];return J.push(Z.slice(K)),J}function T2(Z){let J=[],$="",X="",z=null,Q=!1,K=()=>{if(!Q)return;let G=X.replace(/^"?\$\{?HOME\}?"?(?=\/|$)/,""),Y=$.startsWith("~")&&!/^~(?:\/|$)/.test(X);J.push({text:Y?`./${$}`:$,opaque:/[$\x60*?[]/.test(G)||/[()]/.test(G)}),$="",X="",Q=!1};for(let G=0;G<Z.length;G++){let Y=Z[G];if(z==="'"){if(Y==="'")z=null;else $+=Y;X+=Y;continue}if(Y==="\\"&&G+1<Z.length){let H=Z[G+1];$+=z==='"'&&!/[$\x60"\\\n]/.test(H)?Y+H:H,X+=Y+H,Q=!0,G++;continue}if(z==='"'){if(Y==='"')z=null;else $+=Y;X+=Y;continue}if(/\s/.test(Y)){K();continue}if(Q=!0,X+=Y,Y==="'"||Y==='"')z=Y;else $+=Y}if(z)X+="$";return K(),J}function C2(Z){let J=Z.trim();if(!J)return"read";let $=$Z(J);if(!$.trustworthy||$.substitutions.length||/\$\{[^}]*[|;()]|[<>]\(/.test(J))return w0(J);let X=$.segments.map((z)=>{let Q=u(z);if(Q<0)return"read";let K=z.words.filter((Y,H)=>H>=Q&&!z.redirects.has(H)).map((Y)=>A(Y.text)),G=K[0].split("/").pop();if(["rm","rmdir","unlink","shred","rmtree","del"].includes(G??""))return"delete";if(G==="trash"){let Y=1;while(Y<K.length&&K[Y].startsWith("-")){let H=K[Y++];if(["--help","-h","--version","-V","--identity"].includes(H))return"read";if(["--agent","--spool","--files","--info","--config"].includes(H))Y++;else if(!["--json","--local"].includes(H))return"delete"}if(Y===K.length||["status","doctor","list","info","get","pending","station","setup","backup","storage","retrieval","backup-storage","backup-inspect","backup-refresh","help"].includes(K[Y]))return"read";if(["restore","restore-capsule"].includes(K[Y]))return"write";return"delete"}if(G==="git"){let Y=sZ(z,Q);if(Y==="clean"||Y==="rm")return"delete";return Y==="clone"||Y==="init"?"write":"read"}if(G==="env"&&FZ(qZ(K)))return"delete";if(["sh","bash","zsh","dash","ksh","ash","eval","source",".","su","runuser","env","xargs","find","busybox","toybox"].includes(G??""))return w0(K.join(" "));if(["mkdir","mkfile","touch","mv","cp","ln","tee","install","dd","rsync","scp","truncate"].includes(G??""))return"write";if(G==="curl"||G==="wget")return l0.test(K.join(" "))?"write":"read";if(G==="sed"&&K.slice(1).some((Y)=>/^-i|^--in-place(?:=|$)/.test(Y)))return"write";if(["python","python3","node","bun"].includes(G??"")&&K.some((Y)=>Y==="-c"||Y==="-e"))return"write";return"read"});return X.includes("delete")?"delete":X.includes("write")?"write":"read"}function w0(Z){let J=Z.trim();if(!J)return"read";if(/(?:^|\s)\\?(?:[^\s;&|<>()]*\/)?(?:rm|rmdir|unlink|shred|trash|rmtree|del)(?:\s|$)/.test(J))return"delete";if(/\bgit\b/.test(J)){if(/\bgit\b[^;&|]*\b(?:clean|rm)\b/.test(J))return"delete";if(/\bgit\b[^;&|]*\b(?:clone|init)\b/.test(J))return"write";return"read"}if(/(?:^|\s)\\?(?:[^\s;&|<>()]*\/)?(?:mkdir|mkfile|touch|mv|cp|ln|tee|install|dd)(?:\s|$)/.test(J))return"write";if(/\b(?:curl|wget)\b/.test(J)){if(l0.test(J))return"write";return"read"}if(/\bsed\b/.test(J)){if(/(?:^|\s)-i\S*(?:\s|$)|--in-place(?:\s|$)/.test(J))return"write"}else if(/\b(?:rsync|scp|truncate)\b/.test(J))return"write";else if(/(?:^|\s)(?:python3?|node|bun)\b[^;&|]*\s+-[ce](?:\s|$)/.test(J))return"write";if(/(?:^|[;&|])\s*echo\b/.test(J)||/>/.test(m0(J)))return"write";return"read"}function TZ(Z,J){let $=Z.trim(),X=$.match(/^(['"])([\s\S]*)\1$/);if(X)$=X[2];if($=$.replace(/^"*\$HOME"*\//,"$HOME/"),$=$.replace(/^"*\$\{HOME\}"*\//,"${HOME}/"),$==="~")return J;if($.startsWith("~/"))return JZ(J,$.slice(2));if($==="$HOME"||$==="${HOME}")return J;if($.startsWith("$HOME/"))return JZ(J,$.slice(6));if($.startsWith("${HOME}/"))return JZ(J,$.slice(8));let z=$.match(new RegExp(`^"*${tZ(J)}"*/`));if(z)return J+"/"+$.slice(z[0].length);return Z}function I2(Z){let J=Z.trim();while(J.startsWith("(")||J.startsWith("{"))J=J.slice(1).trim();while(J.endsWith(")")||J.endsWith("}"))J=J.slice(0,-1).trim();return J}function tZ(Z){return Z.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function x0(Z,J,$){if(Z===void 0)return[$];let X=TZ(Z,$),z=X.match(/^(['"])(.*)\1$/);if(z)X=z[2];if(!X||X==="-"||/[$\x60*?[\]{}~\\'"]/.test(X))return null;return[...new Set(J.map((Q)=>CZ(Q,X)))]}var D2=/(?:^|\s)(\.\.?|[^\s"';&|<>()\x60/]+(?:\/[^\s"';&|<>()\x60]*)?)\s*$/;function A2(Z,J,$){if(!Z)return[];let X=u0(J),z=(U)=>X.some((V)=>U===V||U.startsWith(`${V}${oZ}`)),Q=Z.split(/(\s*&&\s*|\s*\|\|\s*|;\s*|\n+)/),K=[],G=[$],Y=new Set([$]),H=!0,B=[];for(let U=0;U<Q.length;U+=2){let V=U===0?"":Q[U-1].trim();if(V==="||")H=!1;else if(V!=="&&")H=!0;if(V!=="&&"||!H)G=[...Y];let I=Q[U],j=(I.match(/^[\s({]*/)?.[0].match(/\(/g)??[]).length,P=(I.match(/[\s)}]*$/)?.[0].match(/\)/g)??[]).length;for(let q=0;q<j;q++)B.push({current:[...G],reachable:new Set(Y),certain:H});let M=I2(I),_=M.match(/^cd(?:\s+(?:-[A-Za-z@]+|--))*(?:\s+([^\s;&|<>()\x60]+))?$/),g=_?null:M.match(/(?:^|\s)cd(?:\s+(?:-[A-Za-z]+|--))*\s+([^\s;&|<>()\x60]+)/);if(g){for(let q of x0(g[1],G,J)??[])Y.add(q);H=!1,G=[...Y]}let h=tZ(J),d=X.map((q)=>tZ(q.slice(J.length+1))).join("|"),D=new RegExp(`(?:~|\\$HOME"*|\\$\\{HOME\\}"*|${h}"*)/(?:${d})`),O=new RegExp(`(${D.source})([^\\s"';&|<>()\`]*|$)`,"g"),v=(q)=>{let w=[],R;O.lastIndex=0;while((R=O.exec(q))!==null)w.push(XZ(TZ(R[0],J)).replace(/\/+$/,""));return w},S=W2(M).map((q)=>{let w=[],R=!1,r=m0(q).replace(j2,(KZ,i)=>{if(!i||/[$\x60]/.test(i))R=!0;if(i)w.push(i);return" "}),QZ=C2(r);return{commandPart:r,operands:w,op:R&&QZ==="read"?"write":QZ}}),U1=S.some((q)=>q.op==="delete")?"delete":S.some((q)=>q.op==="write")?"write":"read",I0=G.filter(z);for(let q of S){let w=!1;for(let L of v(q.commandPart))w=!0,K.push({path:L,op:U1});for(let L of q.operands){let f=v(L);for(let y of f)K.push({path:y,op:"write"});if(f.length>0)continue;for(let y of I0)K.push({path:XZ(CZ(y,TZ(L,J))),op:"write"})}if(w||q.op==="read")continue;let R=T2(q.commandPart),r=1,QZ=(R[0]?.text??"").split("/").pop();if(QZ==="trash"&&["put","rm","delete","remove"].includes(R[1]?.text??""))r=2;if(QZ==="git"){let L=R.findIndex((f,y)=>y>0&&(f.text==="rm"||f.text==="clean"));if(L>0)r=L+1}let KZ=!1,i=R.slice(r).flatMap((L)=>{if(!KZ&&L.text==="--")return KZ=!0,[];if(!KZ&&L.text.startsWith("-"))return[];return KZ=!0,[L.opaque?".":L.text]}),V1=q.op==="delete"||/(?:^|\s)\\?(?:[^\s;&|<>()]*\/)?(?:mkdir|mkfile|touch|tee|truncate|install|mv)(?:\s|$)/.test(q.commandPart)||/(?:^|\s)sed(?:\s|$)/.test(q.commandPart)&&/(?:^|\s)(?:-i\S*|--in-place\S*)(?:\s|$)/.test(q.commandPart),GZ=[];if(V1)for(let L of i.length>0?i:["."])GZ.push({operand:L,op:q.op});else{let L=q.commandPart.match(D2),f=R.at(-1);if(GZ.push({operand:L?L[1]:!f||f.opaque?".":f.text,op:q.op}),/(?:^|\s)(?:cp|ln|install|mv)(?:\s|$)/.test(q.commandPart))R.forEach((y,D0)=>{let o,uZ=y.text.match(/^-[A-Za-z]*?t(.*)$/),DZ=y.text.match(/^(--t[a-z-]*)(?:=(.*))?$/);if(uZ&&!y.text.startsWith("--"))o=uZ[1]?{text:uZ[1],opaque:y.opaque}:R[D0+1];else if(DZ&&"--target-directory".startsWith(DZ[1]))o=DZ[2]!==void 0?{text:DZ[2],opaque:y.opaque}:R[D0+1];if(o)GZ.push({operand:o.opaque||!o.text?".":o.text,op:q.op})});if(/(?:^|\s)(?:python3?|node|bun)\b[^;&|]*\s+-[ce](?:\s|$)/.test(q.commandPart))GZ.push({operand:".",op:q.op})}for(let L of I0)for(let{operand:f,op:y}of GZ)K.push({path:XZ(CZ(L,TZ(f,J))),op:y})}if(_){let q=x0(_[1],G,J);if(q){for(let w of q)Y.add(w);if(H)G=q}else H=!1}for(let q=0;q<P&&B.length>0;q++)({current:G,reachable:Y,certain:H}=B.pop())}return K}var L2=/^\s*\*\*\*\s+(Add File|Update File|Delete File):\s+(\S+)\s*$/gm;function P2(Z,J){let $=[],X;while((X=L2.exec(Z))!==null){let z=X[1]==="Delete File"?"delete":"write";$.push({path:XZ(CZ(J,X[2])),op:z})}return $}function c0(Z,J={}){let $=[];if(Z.hook_event_name!=="PreToolUse")return{output:{continue:!0},warnings:$};let X=typeof Z.tool_name==="string"?Z.tool_name:"",z=J.home??g0(),Q=u0(z),K=q2(process.env,z);if(X==="Bash"){let V=e(Z);if(!V)return{output:{continue:!0},warnings:$};let I=typeof Z.cwd==="string"&&Z.cwd?Z.cwd:process.cwd(),j=A2(V,z,I);for(let P of j)for(let M of Q){let _=rZ(P.path,M,K,P.op);if(_.blocked)return{output:{decision:"block",reason:_.reason},warnings:$}}return{output:{continue:!0},warnings:$}}let G=Z.tool_input??{},Y=typeof Z.cwd==="string"&&Z.cwd?Z.cwd:process.cwd();if(H2.has(X)){let V=typeof G.patch==="string"?G.patch:"";if(!V)return{output:{continue:!0},warnings:$};let I=P2(V,Y);for(let j of I)for(let P of Q){let M=rZ(j.path,P,K,j.op);if(M.blocked)return{output:{decision:"block",reason:M.reason},warnings:$}}return{output:{continue:!0},warnings:$}}if(!G2.has(X))return{output:{continue:!0},warnings:$};let H=typeof G.file_path==="string"?G.file_path:typeof G.path==="string"?G.path:typeof G.notebook_path==="string"?G.notebook_path:null;if(!H)return{output:{continue:!0},warnings:$};let B=TZ(H,z),U=h0(B)?XZ(B):CZ(Y,B);for(let V of Q){let I=rZ(U,V,K,"write");if(I.blocked)return{output:{decision:"block",reason:I.reason},warnings:$}}return{output:{continue:!0},warnings:$}}var O2=new Set(["id","node_id","databaseId","fullDatabaseId","number","state","state_reason","stateReason","status","conclusion","bucket","name","login","sha","oid","abbreviatedOid","ref","headRefName","baseRefName","headRefOid","baseRefOid","head_sha","headSha","head_branch","headBranch","merged","mergeable","mergeable_state","mergeStateStatus","draft","isDraft","locked","isLocked","title","displayTitle","display_title","created_at","updated_at","closed_at","merged_at","started_at","completed_at","submitted_at","pushed_at","published_at","createdAt","updatedAt","closedAt","mergedAt","startedAt","completedAt","submittedAt","pushedAt","publishedAt","tag_name","tagName","total_count","totalCount","count","additions","deletions","changes","changed_files","changedFiles","filename","path","event","workflow","workflowName","workflow_id","workflowDatabaseId","run_id","run_number","run_attempt","attempt","check_suite_id","default_branch","defaultBranch","full_name","fullName","nameWithOwner","private","isPrivate","visibility","archived","isArchived","fork","isFork","isRequired","is_required","required","context","type","__typename","role","permission","size","reviewDecision","author_association","authorAssociation","isCrossRepository","maintainerCanModify","maintainer_can_modify","commentsCount","language","digest","size_in_bytes","run_started_at","date","wait_timer","ahead_by","behind_by","total_commits","expired","current_user_can_approve","environment","html_url"]),E2=new Set(["payload","client_payload","inputs","config","metadata","output","properties","custom_properties","variables"]),s0=new Set(["length","utf8bytelength","not","type","any","all","isempty","empty","test","startswith","endswith","contains","inside","has","in","index","rindex","indices","floor","ceil","round","fabs","sqrt","isinfinite","isnan","isnormal","infinite","nan","true","false","null"]),a0=new Set(["tostring","tojson","ascii_downcase","ascii_upcase","ltrimstr","rtrimstr","trim","ltrim","rtrim","sort","unique","reverse","first","last","min","max","add","flatten","join","split","values","nulls","strings","numbers","booleans","arrays","objects","iterables","scalars","sort_by","group_by","unique_by","min_by","max_by","select","limit","to_entries","from_entries"]),R2=new Set(["test","startswith","endswith","contains","inside","has","in","index","rindex","indices","ltrimstr","rtrimstr","join","split","flatten","limit"]),y2=new Set(["@text","@json","@csv","@tsv","@sh","@base64","@uri","@html"]);class $0 extends Error{}var T=()=>{throw new $0};function eZ(Z,J){let $=[],X="",z=J+1;while(z<Z.length){let Q=Z[z];if(Q==='"'){if(X)$.push(X);return{end:z+1,parts:$}}if(Q==="\\"){let K=Z[z+1];if(K==="("){let G=1,Y=z+2;while(Y<Z.length&&G>0){let H=Z[Y];if(H==='"'){Y=eZ(Z,Y).end;continue}if(H==="(")G++;else if(H===")")G--;Y++}if(G!==0)T();if(X)$.push(X),X="";$.push({src:Z.slice(z+2,Y-1)}),z=Y;continue}if(K===void 0)T();X+=K,z+=2;continue}X+=Q,z++}return T()}function n0(Z){let J=[],$=0,X=["?//","|=","+=","-=","*=","/=","%=","//=","==","!=","<=",">=","//","|",",","+","-","*","/","%","<",">","=","(",")","[","]","{","}",":",";","?"];while($<Z.length){let z=Z[$];if(/\s/.test(z)){$++;continue}if(z==="#")T();if(z==='"'){let Y=eZ(Z,$);J.push({t:"str",parts:Y.parts}),$=Y.end;continue}if(z==="."){if(Z[$+1]===".")T();let Y=/^[A-Za-z_][A-Za-z0-9_]*/.exec(Z.slice($+1));if(Y){J.push({t:"field",name:Y[0]}),$+=1+Y[0].length;continue}if(Z[$+1]==='"'){let H=eZ(Z,$+1);if(H.parts.some((B)=>typeof B!=="string"))T();J.push({t:"field",name:H.parts.join("")}),$=H.end;continue}J.push({t:"dot"}),$++;continue}if(z==="$"||z==="@"){let Y=/^[@$][A-Za-z_][A-Za-z0-9_]*/.exec(Z.slice($));if(!Y||z==="$")T();J.push({t:"fmt",name:Y[0]}),$+=Y[0].length;continue}let Q=/^[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/.exec(Z.slice($));if(Q){J.push({t:"num"}),$+=Q[0].length;continue}let K=/^[A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)*/.exec(Z.slice($));if(K){J.push({t:"ident",name:K[0]}),$+=K[0].length;continue}let G=X.find((Y)=>Z.startsWith(Y,$));if(!G)T();J.push({t:"op",v:G}),$+=G.length}return J}var v2=new Set(["if","then","elif","else","end","as","def","reduce","foreach","try","catch","label","import","include","and","or","__loc__","error","input","inputs","debug","stderr","env","halt","halt_error","input_filename","input_line_number","recurse","paths","leaf_paths","getpath","setpath","delpaths","del","path","splits","sub","gsub","capture","match","scan","keys","keys_unsorted","map_values","with_entries","tostream","fromstream","truncate_stream","walk","tonumber","implode","explode","ascii","fromjson"]);class J0{tokens;at=0;constructor(Z){this.tokens=Z}parse(){if(this.tokens.length===0)T();let Z=this.pipe();if(this.at!==this.tokens.length)T();return Z}peek(){return this.tokens[this.at]}isOp(Z){let J=this.peek();return!!J&&J.t==="op"&&J.v===Z}isIdent(Z){let J=this.peek();return!!J&&J.t==="ident"&&J.name===Z}expectOp(Z){if(!this.isOp(Z))T();this.at++}pipe(){let Z=this.comma();if(this.isIdent("as"))T();if(this.isOp("|"))this.at++,Z={k:"pipe",a:Z,b:this.pipe()};return Z}comma(){let Z=this.alt();while(this.isOp(","))this.at++,Z={k:"comma",a:Z,b:this.alt()};return Z}alt(){let Z=this.or();if(this.isOp("//"))return this.at++,{k:"alt",a:Z,b:this.alt()};return Z}or(){let Z=this.and();while(this.isIdent("or"))this.at++,Z={k:"bool",a:Z,b:this.and()};return Z}and(){let Z=this.cmp();while(this.isIdent("and"))this.at++,Z={k:"bool",a:Z,b:this.cmp()};return Z}cmp(){let Z=this.add();for(let J of["==","!=","<=",">=","<",">"])if(this.isOp(J))return this.at++,{k:"bool",a:Z,b:this.add()};for(let J of["=","|=","+=","-=","*=","/=","%=","//=","?//"])if(this.isOp(J))T();return Z}add(){let Z=this.mul();while(this.isOp("+")||this.isOp("-"))this.at++,Z={k:"arith",a:Z,b:this.mul()};return Z}mul(){let Z=this.unary();while(this.isOp("*")||this.isOp("/")||this.isOp("%"))this.at++,Z={k:"arith",a:Z,b:this.unary()};return Z}unary(){if(this.isOp("-"))return this.at++,{k:"neg",of:this.postfix()};return this.postfix()}objectValue(){let Z=this.unary();if(this.isOp("|"))this.at++,Z={k:"pipe",a:Z,b:this.objectValue()};return Z}postfix(){let Z=this.primary();for(;;){let J=this.peek();if(!J)return Z;if(J.t==="field"){this.at++,Z={k:"field",name:J.name,of:Z};continue}if(J.t==="dot"&&this.tokens[this.at+1]?.t==="op"&&this.tokens[this.at+1].v==="["){this.at++;continue}if(J.t==="op"&&J.v==="?"){this.at++,Z={k:"opt",of:Z};continue}if(J.t==="op"&&J.v==="["){Z=this.bracket(Z);continue}return Z}}bracket(Z){if(this.expectOp("["),this.isOp("]"))return this.at++,{k:"iter",of:Z};if(this.isOp(":")){if(this.at++,!this.isOp("]"))this.pipe();return this.expectOp("]"),{k:"slice",of:Z}}let J=this.pipe();if(this.isOp(":")){if(this.at++,!this.isOp("]"))this.pipe();return this.expectOp("]"),{k:"slice",of:Z}}return this.expectOp("]"),{k:"index",of:Z,key:J}}primary(){let Z=this.peek();if(!Z)return T();switch(this.at++,Z.t){case"dot":return{k:"dot"};case"field":return{k:"field",name:Z.name};case"num":return{k:"lit"};case"str":return this.stringNode(Z.parts);case"fmt":{if(!y2.has(Z.name))T();let J=this.peek();if(J?.t==="str")return this.at++,{k:"fmt",name:Z.name,str:this.stringNode(J.parts)};return{k:"fmt",name:Z.name}}case"ident":{if(v2.has(Z.name))T();if(Z.name==="map"||s0.has(Z.name)||a0.has(Z.name)){let J=[];if(this.isOp("(")){this.at++,J.push(this.pipe());while(this.isOp(";"))this.at++,J.push(this.pipe());this.expectOp(")")}return{k:"call",name:Z.name,args:J}}return T()}case"op":{if(Z.v==="("){let J=this.pipe();return this.expectOp(")"),J}if(Z.v==="["){if(this.isOp("]"))return this.at++,{k:"array"};let J=this.pipe();return this.expectOp("]"),{k:"array",of:J}}if(Z.v==="{")return this.object();return T()}}return T()}stringNode(Z){let J=[];for(let $ of Z){if(typeof $==="string")continue;J.push(new J0(n0($.src)).parse())}return J.length===0?{k:"str",parts:J,text:Z.join("")}:{k:"str",parts:J}}object(){let Z=[];if(this.isOp("}"))return this.at++,{k:"object",entries:Z};for(;;){let J=this.peek();if(!J)T();this.at++;let $;if(J.t==="ident")$=J.name;else if(J.t==="str"){if(J.parts.some((X)=>typeof X!=="string"))T();$=J.parts.join("")}else T();if(this.isOp(":"))this.at++,Z.push({key:"static",value:this.objectValue()});else Z.push({key:"static",value:{k:"field",name:$}});if(this.isOp(",")){this.at++;continue}return this.expectOp("}"),{k:"object",entries:Z}}}}function X0(Z){if(typeof Z==="object"&&"union"in Z)return Z.union.every(X0);return Z==="safe"||typeof Z==="object"&&O2.has(Z.leaf)}var IZ=(...Z)=>Z.every(X0)?"safe":"tainted";function Z0(...Z){let J=[];for(let $ of Z)if(typeof $==="object"&&"union"in $)J.push(...$.union);else J.push($);return J.length===1?J[0]:{union:J}}function yZ(Z,J){return typeof Z==="object"&&"union"in Z?Z0(...Z.union.map(J)):J(Z)}var vZ=(Z)=>typeof Z==="object"&&("leaf"in Z)&&E2.has(Z.leaf),l=(Z)=>yZ(Z,(J)=>J==="safe"?"safe":J==="tainted"||vZ(J)?"tainted":"unknown");function p0(Z){return Z.k==="lit"||Z.k==="str"&&Z.parts.length===0||Z.k==="call"&&["true","false","null"].includes(Z.name)&&Z.args.length===0}function d0(Z,J,$){return yZ(Z,(X)=>{if(X==="safe"||X==="tainted")return X;if(vZ(X))return"tainted";if(X==="root"&&$.rootScalars.has(J))return"safe";return{leaf:J}})}function C(Z,J,$){switch(Z.k){case"dot":return J;case"field":return d0(Z.of?C(Z.of,J,$):J,Z.name,$);case"iter":return l(C(Z.of,J,$));case"index":{let X=C(Z.of,J,$);if(Z.key.k==="str"&&Z.key.text!==void 0)return d0(X,Z.key.text,$);if(Z.key.k==="lit")return l(X);return C(Z.key,J,$),"tainted"}case"slice":return yZ(C(Z.of,J,$),(X)=>X==="root"?"unknown":vZ(X)?"tainted":X);case"opt":return C(Z.of,J,$);case"lit":return"safe";case"str":return IZ(...Z.parts.map((X)=>C(X,J,$)));case"fmt":return Z.str?C(Z.str,J,$):J;case"array":return Z.of?IZ(C(Z.of,J,$)):"safe";case"object":return IZ(...Z.entries.map((X)=>C(X.value,J,$)));case"pipe":return C(Z.b,C(Z.a,J,$),$);case"comma":return Z0(C(Z.a,J,$),C(Z.b,J,$));case"alt":return Z0(C(Z.a,J,$),C(Z.b,J,$));case"bool":return C(Z.a,J,$),C(Z.b,J,$),"safe";case"arith":return IZ(C(Z.a,J,$),C(Z.b,J,$));case"neg":return C(Z.of,J,$);case"call":return S2(Z,J,$)}}var i0=(Z)=>yZ(Z,(J)=>J==="root"?"unknown":vZ(J)?"tainted":J);function S2(Z,J,$){let{name:X,args:z}=Z;if(R2.has(X)&&X!=="limit"&&!z.every(p0))T();switch(X){case"map":if(z.length!==1)T();return IZ(C(z[0],l(J),$));case"select":if(z.length!==1)T();return C(z[0],J,$),J;case"limit":if(z.length!==2||!p0(z[0]))T();return C(z[1],J,$);case"first":case"last":if(z.length>1)T();return z.length===1?C(z[0],J,$):l(J);case"min":case"max":return l(J);case"sort_by":case"group_by":case"unique_by":if(z.length!==1)T();return C(z[0],l(J),$),i0(J);case"min_by":case"max_by":if(z.length!==1)T();return C(z[0],l(J),$),l(J);case"any":case"all":case"isempty":for(let Q of z)C(Q,"unknown",$);return"safe"}if(s0.has(X))return"safe";if(a0.has(X))return i0(J);return T()}function r0(Z,J={}){if(typeof Z!=="string"||!Z.trim()||Z.length>4096)return!1;try{let $=new J0(n0(Z)).parse();return X0(C($,"root",{rootScalars:J.rootScalars??new Set}))}catch($){if($ instanceof $0)return!1;return!1}}var V0="composite output withheld: signed-link shape; use bounded scalar projections per class disposition 799310",N={refused:!1},F=(Z)=>({refused:!0,rule:Z}),z0=new Set(["additions","assignees","author","baseRefName","baseRefOid","changedFiles","closed","closedAt","createdAt","deletions","files","fullDatabaseId","headRefName","headRefOid","headRepository","headRepositoryOwner","id","isCrossRepository","isDraft","labels","maintainerCanModify","mergeCommit","mergeStateStatus","mergeable","mergedAt","mergedBy","number","potentialMergeCommit","reactionGroups","reviewDecision","reviewRequests","state","title","updatedAt","url"]),Y0=new Set(["assignees","author","closed","closedAt","createdAt","id","isPinned","labels","number","reactionGroups","state","stateReason","subIssuesSummary","title","updatedAt","url"]),o0=new Set(["apiUrl","author","createdAt","databaseId","id","isDraft","isImmutable","isLatest","isPrerelease","name","publishedAt","tagName","tarballUrl","targetCommitish","uploadUrl","url","zipballUrl"]),t0=new Set(["answerChosenAt","answerChosenBy","answered","author","closed","closedAt","createdAt","id","labels","locked","number","reactionGroups","state","stateReason","title","updatedAt","url"]),b2=new Set(["assignees","author","authorAssociation","closedAt","commentsCount","createdAt","id","isDraft","isLocked","isPullRequest","labels","number","repository","state","title","updatedAt","url"]),f2=new Set(["author","committer","id","parents","repository","sha","url"]),k2=new Set(["bucket","completedAt","event","name","startedAt","state","workflow"]);function $1(Z){let J=[];for(let $=0;$<Z.length;$++){let X=Z[$];if(X==="\\"){$++;continue}if(X==="'"){let z=Z.indexOf("'",$+1);if(z<0)return!1;$=z;continue}if(X==='"'){$++;while($<Z.length&&Z[$]!=='"')$+=Z[$]==="\\"?2:1;continue}if(X==="{"){J.push({list:!1});continue}if(J.length===0)continue;if(X===","||X==="."&&Z[$+1]===".")J[J.length-1].list=!0;else if(X==="}"&&J.pop().list)return!0}return!1}function zZ(Z){let J="",$=!1,X=0,z=(Q)=>Z[Q]==="`"||Z[Q]==="$"&&/[A-Za-z_{(@*#?$!0-9-]/.test(Z[Q+1]??"");while(X<Z.length){let Q=Z[X];if(Q==="\\"){J+=Z[X+1]??"",X+=2;continue}if(Q==="$"&&Z[X+1]==="'"){$=!0,J+=Q,X++;continue}if(Q==="'"){let K=Z.indexOf("'",X+1);if(K<0){$=!0,J+=Z.slice(X+1);break}J+=Z.slice(X+1,K),X=K+1;continue}if(Q==='"'){X++;while(X<Z.length&&Z[X]!=='"'){if(Z[X]==="\\"&&/[$`"\\\n]/.test(Z[X+1]??"")){J+=Z[X+1],X+=2;continue}if(z(X))$=!0;J+=Z[X],X++}if(X>=Z.length)$=!0;X++;continue}if(z(X)||Q==="*"||Q==="?"||Q==="[")$=!0;J+=Q,X++}if(!$&&Z.includes("{")&&$1(Z))$=!0;return{value:J,opaque:$,raw:Z}}var a=(Z)=>Z.slice(Z.lastIndexOf("/")+1);function G0(Z,J="word"){let $=[],X=!1;for(let z=0;z<Z.length;z++){let Q=Z[z];if(Q==="\\"){z++;continue}if(J==="word"&&Q==="'"&&!X){let K=Z.indexOf("'",z+1);if(K<0)return{bodies:$,complete:!1};z=K;continue}if(J==="word"&&Q==='"'){X=!X;continue}if(Q==="`"){let K=z+1;while(K<Z.length&&Z[K]!=="`")K+=Z[K]==="\\"?2:1;if(K>=Z.length)return{bodies:$,complete:!1};$.push(Z.slice(z+1,K)),z=K;continue}if(Q==="$"&&Z[z+1]==="("){let K=_Z(Z,z+1);if(K<0)return{bodies:$,complete:!1};$.push(Z.slice(z+2,K-1)),z=K-1}}return{bodies:$,complete:!0}}function YZ(Z,J){let $=new Set(J.values),X={flags:[],positionals:[]},z=(Q,K)=>({value:Q,opaque:K.opaque,raw:K.raw});for(let Q=0;Q<Z.length;Q++){let K=Z[Q],G=K.value;if(G==="--"){X.positionals.push(...Z.slice(Q+1));break}if(G.startsWith("--")&&G.length>2){let Y=G.indexOf("="),H=Y>=0?G.slice(0,Y):G;if(Y>=0)X.flags.push({name:H,value:z(G.slice(Y+1),K),inline:!0});else if($.has(H)){let B=Z[Q+1];X.flags.push(B?{name:H,value:B}:{name:H,missing:!0}),Q++}else X.flags.push({name:H});continue}if(G.startsWith("-")&&G.length>1){for(let Y=1;Y<G.length;Y++){let H="-"+G[Y],B=G.slice(Y+1);if(B.startsWith("=")){X.flags.push({name:H,value:z(B.slice(1),K),inline:!0});break}if(!$.has(H)){X.flags.push({name:H});continue}if(B)X.flags.push({name:H,value:z(B,K),inline:!0});else{let U=Z[Q+1];X.flags.push(U?{name:H,value:U}:{name:H,missing:!0}),Q++}break}continue}X.positionals.push(K)}return X}function E(Z,...J){let $=!1;for(let X of Z.flags){if(!J.includes(X.name))continue;if(!X.inline){$=!0;continue}let z=X.value;$=!z.opaque&&["1","t","T","TRUE","true","True"].includes(z.value)}return $}var c=(Z,...J)=>Z.flags.filter(($)=>J.includes($.name)),b=["-R","--repo","-q","--jq","-t","--template","--json"],n={"pr view":{values:b},"issue view":{values:b},"release view":{values:b},"discussion view":{values:[...b,"--after","-L","--limit","--order"]},"pr list":{values:[...b,"--app","-a","--assignee","-A","--author","-B","--base","-H","--head","-l","--label","-L","--limit","-S","--search","-s","--state"]},"issue list":{values:[...b,"--app","-a","--assignee","-A","--author","-l","--label","-L","--limit","--mention","-m","--milestone","-S","--search","-s","--state"]},"discussion list":{values:[...b,"--after","-A","--author","-c","--category","-l","--label","-L","--limit","--order","-S","--search","--sort","-s","--state"]},"release list":{values:[...b,"-L","--limit","-O","--order"]},"pr status":{values:b},"issue status":{values:b},"pr checks":{values:[...b,"-i","--interval"]},"pr diff":{values:["-R","--repo"]},search:{values:[...b,"--app","--assignee","--author","--author-date","--author-email","--author-name","--base","--checks","--closed","--commenter","--committer","--committer-date","--committer-email","--committer-name","--created","--hash","--head","--interactions","--involves","--label","--language","--limit","-L","--match","--mentions","--merged-at","--milestone","--no-assignee","--no-label","--no-milestone","--no-project","--order","--owner","--parent","--project","--reactions","--review","--review-requested","--reviewed-by","--sort","--state","--team-mentions","--tree","--updated","--visibility","--extension","--filename","--size","--forks","--good-first-issue","--help-wanted-issues","--number-topics","--stars","--topic","--followers","--include-forks","--license"]},"project item-list":{values:["--format","-L","--limit","--owner","-q","--jq","-t","--template","--query","--field","--field-id"]},api:{values:["-X","--method","-H","--header","-f","--raw-field","-F","--field","-q","--jq","-t","--template","-p","--preview","--input","--hostname","--cache"]},group:{values:["-R","--repo"]}},w2=new Set(["auth","browse","codespace","cs","gist","org","repo","skill","cache","run","workflow","co","agent-task","alias","attestation","completion","config","copilot","extension","ext","extensions","gpg-key","label","licenses","preview","ruleset","secret","ssh-key","variable","help","version","accessibility","actions","environment","exit-codes","formatting","mintty","reference","telemetry"]),x2=new Set(["pr","issue","release","discussion","search","project","api","status"]),g2=new Set(["create","new","checkout","co","close","comment","edit","lock","merge","ready","reopen","revert","review","unlock","update-branch"]),h2=new Set(["create","new","close","comment","delete","develop","edit","lock","pin","reopen","transfer","unlock","unpin"]),u2=new Set(["create","new","delete","delete-asset","download","edit","upload","verify","verify-asset"]),l2=new Set(["create","new","close","comment","delete","edit","lock","reopen","unlock","answer","unanswer"]);function kZ(Z,J){let $=c(Z,"--json"),X=[],z=!1;for(let Q of $){if(Q.missing){if(J.appendsArgs)z=!0;continue}if(!Q.value)continue;if(Q.value.opaque)z=!0;X.push(...Q.value.value.split(",").map((K)=>K.trim()).filter(Boolean))}return{present:$.length>0,opaque:z,fields:X}}function M0(Z,J){if(E(Z,"--verbose")||c(Z,"-t","--template").length>0)return!1;let $=c(Z,"-q","--jq");return $.length>0&&$.every((X)=>X.value!==void 0&&!X.value.opaque&&r0(X.value.value,{rootScalars:J}))}function F0(Z,J,$,X){let z=kZ(Z,$);if(z.opaque)return F("unclassifiable");if(z.fields.every((Q)=>J.has(Q)))return N;if($.debug)return F("debug-traffic");return M0(Z)?N:F(X)}function SZ(Z,J,$,X,z){let Q=YZ(Z,n[J]);if(E(Q,"-h","--help"))return N;if(E(Q,"-w","--web"))return X.debug?F("debug-traffic"):N;if(z&&E(Q,"-c","--comments"))return F("view-comments");if(!kZ(Q,X).present)return F(X.debug?"debug-traffic":"view-human");return F0(Q,$,X,"json-composite")}function m(Z,J,$,X){let z=YZ(Z,n[J]);if(E(z,"-h","--help"))return N;if(X.debug&&(E(z,"-w","--web")||!kZ(z,X).present))return F("debug-traffic");if(E(z,"-w","--web"))return N;return F0(z,$,X,"json-composite")}function m2(Z,J){let $=YZ(Z,n["pr checks"]);if(E($,"-h","--help"))return N;if(J.debug)return F("debug-traffic");if(E($,"-w","--web"))return N;if(!kZ($,J).present)return F("checks-human");return F0($,k2,J,"checks-json")}function c2(Z){let J=new Set(n.group.values);for(let $=0;$<Z.length;$++){let X=Z[$].value;if(X==="--")return{sub:Z[$+1],rest:Z.slice($+2)};if(!X.startsWith("-")||X==="-")return{sub:Z[$],rest:[...Z.slice(0,$),...Z.slice($+1)]};if(J.has(X))$++}return{rest:Z}}function p2(Z,J,$){let{sub:X,rest:z}=c2(J);if(!X)return $.appendsArgs?F("unclassifiable"):N;if(X.opaque)return F("unclassifiable");let Q=X.value;if((Z==="pr"?g2:Z==="issue"?h2:Z==="release"?u2:Z==="discussion"?l2:new Set).has(Q))return $.debug?F("debug-traffic"):N;if($.appendsArgs&&!(Z==="project"&&Q!=="item-list"))return F("unclassifiable");switch(Z){case"pr":if(Q==="view")return SZ(z,"pr view",z0,$,!0);if(Q==="list"||Q==="ls")return m(z,"pr list",z0,$);if(Q==="status")return m(z,"pr status",z0,$);if(Q==="checks")return m2(z,$);if(Q==="diff"){let G=YZ(z,n["pr diff"]);if(E(G,"-h","--help"))return N;if($.debug)return F("debug-traffic");return E(G,"--patch")?F("composite-other"):N}return F("unclassifiable");case"issue":if(Q==="view")return SZ(z,"issue view",Y0,$,!0);if(Q==="list"||Q==="ls")return m(z,"issue list",Y0,$);if(Q==="status")return m(z,"issue status",Y0,$);return F("unclassifiable");case"release":if(Q==="view")return SZ(z,"release view",o0,$,!1);if(Q==="list"||Q==="ls")return m(z,"release list",o0,$);return F("unclassifiable");case"discussion":if(Q==="view")return SZ(z,"discussion view",t0,$,!0);if(Q==="list"||Q==="ls")return m(z,"discussion list",t0,$);return F("unclassifiable");case"search":if(Q==="prs"||Q==="issues")return m(z,"search",b2,$);if(Q==="commits")return m(z,"search",f2,$);if(Q==="code"||Q==="repos")return $.debug?F("debug-traffic"):N;return F("unclassifiable");case"project":{if($.debug)return F("debug-traffic");if(Q!=="item-list")return N;let G=YZ(z,n["project item-list"]);if(E(G,"-h","--help"))return N;let Y=c(G,"--format");if(Y.some((H)=>!H.value||H.value.opaque))return F("unclassifiable");if(Y.every((H)=>H.value.value!=="json"))return N;return M0(G)?N:F("composite-other")}}return F("unclassifiable")}var J1=new Set(["pulls","issues","commits","check-runs","check-suites","statuses","comments","releases","compare","events","discussions","deployments","milestones"]);function d2(Z){let J=Z.value.trim(),$=/^https?:\/\/([^/]+)(\/.*)?$/i.exec(J);if($){let Q=$[1].toLowerCase(),K=$[2]??"/";if(Q==="api.github.com"||/^api\./.test(Q))J=K;else if(/^\/api\/v3(?:\/|$)/i.test(K))J=K.replace(/^\/api\/v3/i,"");else if(/^\/api\/graphql(?:[/?#]|$)/i.test(K))J="graphql";else return null}J=J.replace(/[?#].*$/,"");let X=[],z=[];for(let Q of J.split("/")){let K;try{K=decodeURIComponent(Q)}catch{return null}for(let G of K.split("/")){if(G===""||G===".")continue;if(G===".."){X.pop(),z.pop();continue}X.push(G.toLowerCase()),z.push(Z.opaque&&/[$`]/.test(G)||/[*?[]/.test(G)||$1(G))}}return{segments:X,opaque:z}}function i2(Z){let J=d2(Z);if(!J)return{kind:"unclassifiable",segments:[]};let{segments:$,opaque:X}=J,z=(Y)=>({kind:Y,segments:$});if($.length===1&&$[0]==="graphql")return z("graphql");if($.length===0)return z(Z.opaque?"unclassifiable":"other");if(X[0])return z("unclassifiable");let Q,K=[];if($[0]==="repos")Q=$.slice(3),K=X.slice(3);else if($[0]==="repositories")Q=$.slice(2),K=X.slice(2);if(Q){if(($[0]==="repos"?X.slice(1,3):X.slice(1,2)).some(Boolean)){let H=X.findIndex(Boolean),B=$.findIndex((U,V)=>V>H&&!X[V]&&s2.has(U));return z(B<0?"unclassifiable":e0($.slice(B),X.slice(B)))}return z(e0(Q,K))}let G=$[0];if(G==="issues")return z("sensitive");if(G==="user"&&$[1]==="issues")return z("sensitive");if(G==="orgs"&&$[2]==="issues")return z("sensitive");if(G==="search")return z($[1]==="issues"||$[1]==="commits"?"sensitive":$[1]!==void 0&&X[1]?"unclassifiable":"other");if(G==="events"||G==="networks")return z("sensitive");if((G==="users"||G==="orgs")&&$.slice(2).some((Y)=>Y==="events"||Y==="received_events"||Y==="discussions"))return z("sensitive");if((G==="users"||G==="orgs")&&$[2]==="projectsv2"&&$[4]==="items")return z("sensitive");if(G==="orgs"&&$[2]==="hooks"&&$[4]==="deliveries"&&$[5]!==void 0||G==="app"&&$[1]==="hook"&&$[2]==="deliveries"&&$[3]!==void 0)return z("sensitive");if(G==="teams"&&$.includes("discussions"))return z("sensitive");if(G==="projects"&&$.includes("cards"))return z("sensitive");if(X.some(Boolean))return z($.some((Y)=>Y==="events"||Y==="discussions"||Y==="issues")?"sensitive":"unclassifiable");return z("other")}var s2=new Set([...J1,"branches","git","actions","hooks","zipball","tarball"]);function e0(Z,J){let[$,X]=Z;if($===void 0)return"other";if(J[0])return"unclassifiable";if($==="issues"&&/^\d+$/.test(X??"")&&Z[2]==="labels")return"other";if(J1.has($))return"sensitive";if($==="branches"){if(Z.length===1||Z.slice(1).some((z)=>z==="protection")||Z[Z.length-1]==="rename")return"other";return J.slice(1).some(Boolean)?"unclassifiable":"sensitive"}if($==="git")return X===void 0||X==="commits"||X==="tags"?"sensitive":J[1]?"unclassifiable":"other";if($==="actions"){if(X===void 0)return"sensitive";if(J[1])return"unclassifiable";if(X==="runs"||X==="jobs"){if(Z[Z.length-1]==="logs"&&Z.length>=4&&!J.some(Boolean))return"download";return"sensitive"}if(X==="workflows"&&Z[3]==="runs")return"sensitive";if(X==="artifacts"&&Z.length===4)return"download";return"other"}if($==="hooks"&&Z[2]==="deliveries"&&Z[3]!==void 0)return"sensitive";if($==="zipball"||$==="tarball")return"download";return"other"}function a2(Z,J){if(J[0]!=="repos"||J.length<5)return!1;let $=J.slice(3),X=(Q)=>/^\d+$/.test($[Q]??""),z=(...Q)=>$.length===Q.length&&Q.every((K,G)=>typeof K==="number"?X(G):$[G]===K);if(Z==="DELETE")return z("issues","comments",0)||z("pulls","comments",0)||z("comments",0)||z("issues","comments",0,"reactions",0)||z("pulls","comments",0,"reactions",0)||z("issues",0,"reactions",0)||z("issues",0,"labels")||$.length===4&&$[0]==="issues"&&X(1)&&$[2]==="labels"||z("releases",0)||z("releases","assets",0)||z("actions","runs",0)||z("actions","runs",0,"logs")||z("deployments",0)||z("milestones",0)||z("issues",0,"lock");if(Z==="POST")return $.length===4&&$[0]==="actions"&&$[1]==="runs"&&X(2)&&["rerun","rerun-failed-jobs","cancel","force-cancel","approve"].includes($[3])||z("actions","jobs",0,"rerun")||z("issues",0,"labels");if(Z==="PUT")return z("pulls",0,"merge")||z("pulls",0,"update-branch")||z("issues",0,"labels")||z("issues",0,"lock");if(Z==="GET")return z("pulls",0,"merge");return!1}function n2(Z){if(Z[0]!=="repos"||Z.length!==5||!/^\d+$/.test(Z[4]))return;if(Z[3]==="pulls")return new Set(["commits","comments","review_comments"]);if(Z[3]==="issues")return new Set(["comments"]);return}var r2=new Set(["body","bodyText","bodyHTML","bodyUrl","bodyResourcePath","comments","reviews","latestReviews","latestOpinionatedReviews","reviewThreads","url","permalink","summary","text","description","message","messageBody","messageHeadline","commitBody","commitHeadline","annotations","rawDetails","note","diff","readme","payload"]),o2=new Set(["checkRuns","checkRun","checkSuites","checkSuite","contexts","statusCheckRollup","annotations","CheckRun","CheckSuite","StatusContext","CheckAnnotation","latestStatus","status","statuses"]);function t2(Z){let J=[],$=0;while($<Z.length){let X=Z[$];if(/[\s,\uFEFF]/.test(X)){$++;continue}if(X==="#"){let K=Z.indexOf(`
|
|
11
|
+
`,$);$=K<0?Z.length:K+1;continue}if(Z.startsWith('"""',$)){let K=$+3;for(;;){let G=Z.indexOf('"""',K);if(G<0)return null;if(Z[G-1]==="\\"){K=G+3;continue}$=G+3;break}continue}if(X==='"'){let K=$+1;while(K<Z.length&&Z[K]!=='"'&&Z[K]!==`
|
|
12
|
+
`)K+=Z[K]==="\\"?2:1;if(Z[K]!=='"')return null;$=K+1;continue}let z=/^[_A-Za-z][_0-9A-Za-z]*/.exec(Z.slice($,$+256));if(z){J.push({kind:"name",text:z[0]}),$+=z[0].length;continue}let Q=/^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(Z.slice($,$+64));if(Q){$+=Q[0].length;continue}if(Z.startsWith("...",$)){J.push({kind:"punct",text:"..."}),$+=3;continue}if("{}()[]:!$@=|&".includes(X)){J.push({kind:"punct",text:X}),$++;continue}return null}return J}function e2(Z){let J=t2(Z);if(!J)return!0;let $=[],X="",z=0;for(let Q=0;Q<J.length;Q++){let K=J[Q];if(K.kind==="punct"){if(K.text==="(")z++;else if(K.text===")"){if(z--,z<0)return!0}else if(z===0&&K.text==="{")$.push(X),X="";else if(z===0&&K.text==="}"){if($.pop()===void 0)return!0;X=""}continue}if(z>0)continue;let G=J[Q-1];if(G?.kind==="punct"&&(G.text==="@"||G.text==="$"))continue;if(J[Q+1]?.kind==="punct"&&J[Q+1].text===":")continue;if(K.text==="on"&&G?.kind==="punct"&&G.text==="...")continue;if(G?.kind==="name"&&G.text==="on"){X=K.text;continue}if($.length===0){X=K.text;continue}if(r2.has(K.text)||/(?:Url|URL|HTML|Html)$/.test(K.text))return!0;if(K.text==="title"&&$.some((Y)=>o2.has(Y)))return!0;X=K.text}return z!==0||$.length!==0}function Z3(Z,J){let $=YZ(Z,n.api);if(E($,"-h","--help"))return N;let X=$.positionals[0];if(!X)return J.appendsArgs?F("unclassifiable"):N;let z=i2(X),Q=c($,"-X","--method"),K=Q[Q.length-1],G=c($,"-f","--raw-field","-F","--field","--input"),Y=K?K.value&&!K.value.opaque?K.value.value.toUpperCase():"?":G.length>0?"POST":"GET",H=a2(Y,z.segments),B=N;if(z.kind==="unclassifiable")B=F("unclassifiable");else if(z.kind==="sensitive"&&!H)B=F("api-rest");else if(z.kind==="graphql")B=$3($);if(J.debug||E($,"--verbose")){if(z.kind==="download"&&!B.refused)B=F("api-rest");if(B.refused)return J.debug?F("debug-traffic"):B;return N}if(J.appendsArgs&&z.kind!=="other"&&z.kind!=="download")return F("unclassifiable");if(!B.refused||E($,"--silent"))return N;if(M0($,n2(z.segments)))return N;return B}function $3(Z){if(c(Z,"--input").length>0)return F("unclassifiable");let J=c(Z,"-f","--raw-field","-F","--field"),$=J.filter((X)=>X.value&&/^query=/.test(X.value.value));if($.length===0||J.some((X)=>!X.value))return F("unclassifiable");for(let X of $){let z=X.value.value.slice(6);if((X.name==="-F"||X.name==="--field")&&z.startsWith("@")||X.value.opaque)return F("unclassifiable");if(e2(z))return F("api-graphql")}return N}function fZ(Z,J){let $=0;while($<Z.length&&Z[$].value.startsWith("-")&&!Z[$].opaque){if(["--help","-h","--version"].includes(Z[$].value))return N;$++}if($>=Z.length)return J.appendsArgs?F("unclassifiable"):N;let X=Z[$],z=Z.slice($+1);if(X.opaque)return F("unclassifiable");switch(X.value){case"pr":case"issue":case"release":case"discussion":case"search":case"project":return p2(X.value,z,J);case"api":return Z3(z,J);case"status":return z.length>0&&z.every((Q)=>Q.value==="-h"||Q.value==="--help")?N:F("composite-other");case"help":case"version":return N;case"cs":case"codespace":{let Q=z.findIndex((K)=>K.value==="--");if(z.some((K)=>K.value==="ssh")&&Q>=0&&Q+1<z.length){let K=x(z.slice(Q+1).map((G)=>G.value).join(" "),J.depth+1,J.debug);if(K.refused)return K}break}}if(!w2.has(X.value))return F("unclassifiable");if(J.debug)return F("debug-traffic");return N}var wZ=new Set(["sh","bash","zsh","dash","ksh","ash","mksh","fish"]),J3=new Set(["su","runuser","script","flock"]),X3=new Set(["secrets","op","unbuffer","caffeinate","chronic","strace","ltrace","firejail","bwrap","nsenter","unshare","chroot","sg","retry","parallel","doppler","aws-vault","dotenv","direnv","faketime","taskset","numactl","cgexec","systemd-run","docker","podman","kubectl","mise","asdf","rtx","devbox","gosu","su-exec","setpriv","pkexec","with-contenv","uv","poetry","pipenv","npx","bunx","pnpm","yarn","nix","nix-shell","infisical","chamber","envchain","dotenvx","teller","vault","chrt","coproc","prlimit","ionice","cpulimit","trickle","proxychains","proxychains4","torsocks","tsocks","catchsegv","valgrind"]),z3=new Map([["bun",["x","exec"]],["npm",["exec","x"]],["deno",["run"]]]),Y3=new Set(["-I","-L","-l","-n","-P","-s","-d","-E","-e","-a","--arg-file","--delimiter","--max-args","--max-lines","--max-procs","--max-chars","--replace","--eof","--process-slot-var"]),Q3=new Set(["-b","-c","-D","-E","-e","-F","-I","-i","-J","-L","-l","-m","-O","-o","-p","-Q","-R","-S","-W","-w"]),bZ=/(?:^|[^A-Za-z0-9_.-])gh(?:$|[^A-Za-z0-9_.-])/,K3=/^([A-Za-z_][A-Za-z0-9_]*)=([\s\S]*)$/,G3=6;function Z1(Z,J){let $,X;for(let z of Z){let Q=K3.exec(z.value);if(!Q)continue;let K={...z,value:Q[2]};if(Q[1]==="GH_DEBUG")$=K;else if(Q[1]==="DEBUG")X=K}if($)return $.opaque||!["","0","false","no"].includes($.value);if(X&&(X.opaque||["1","true","yes","api"].includes(X.value)))return!0;return J}function N0(Z){let J=$Z(Z),$=0;return{segments:J.segments.map((z)=>{let Q=z.words[0],K=Z.slice($,Q.start);return $=z.words[z.words.length-1].end,{words:z.words,redirects:z.redirects,sepBefore:K}}),trustworthy:J.trustworthy}}var X1=(Z)=>/(?:^|[^|])\|&?$/.test(Z.trim());function H0(Z,J){if(!wZ.has(Z))return!1;if(q0(J))return!1;for(let $=0;$<J.length;$++){let X=J[$].value;if(X==="--")return $+1>=J.length;if(X==="-"||X==="-s")return!0;if(/^[-+][A-Za-z]*[oO]$/.test(X)){$++;continue}if(!X.startsWith("-")&&!X.startsWith("+"))return!1}return!0}function B0(Z){let{segments:J}=N0(Z.trim());if(J.length!==1)return null;let $=J[0],X=u($);if(X<0)return null;let z=a(zZ($.words[X].text).value),Q=$.words.slice(X+1).filter((K,G)=>!$.redirects.has(G+X+1)).map((K)=>zZ(K.text));if(z==="echo")return Q.filter((K,G)=>!(G===0&&/^-[neE]+$/.test(K.value))).map((K)=>K.value).join(" ");if(z==="printf"){let[K,...G]=Q.filter((H)=>H.value!=="--");if(!K)return null;let Y=0;return K.value.replace(/\\n/g,`
|
|
13
|
+
`).replace(/%s/g,()=>G[Y++]?.value??"")+(Y<G.length?" "+G.slice(Y).map((H)=>H.value).join(" "):"")}if(z==="cat"){let K=Z.indexOf("<<");if(K<0)return null;let G=EZ(Z,K),Y=Z.indexOf(`
|
|
14
|
+
`,K);if(!G||Y<0)return null;let H=RZ(Z,Y,[G]);if(H<0)return null;let B=Z.slice(Y+1,H);return B.slice(0,B.lastIndexOf(G.delimiter)).replace(/\n$/,"")}return null}function Q0(Z,J,$){for(let X of G0(Z).bodies){let z=B0(X);if(z===null)continue;let Q=x(z,J+1,$.debug);if(Q.refused)return Q}return N}function x(Z,J=0,$=!1){if(typeof Z!=="string"||!Z.trim())return N;if(J>G3)return bZ.test(Z)?F("unclassifiable"):N;let{segments:X,trustworthy:z}=N0(Z);if(!z)return bZ.test(Z)?F("unclassifiable"):N;let Q=$,K=(G)=>x(G,J+1,Q);for(let G of U3(Z)){if(G.runsBody){let Y=K(G.body);if(Y.refused)return Y}if(!G.quoted){let Y=G0(G.body,"heredoc");if(!Y.complete&&bZ.test(G.body))return F("unclassifiable");for(let H of Y.bodies){let B=K(H);if(B.refused)return B}}}for(let G=0;G<X.length;G++){let Y=X[G];for(let D of Y.words){let O=G0(D.text);if(!O.complete&&bZ.test(D.text))return F("unclassifiable");for(let v of O.bodies){let S=K(v);if(S.refused)return S}}let H=u(Y),B=Y.words.map((D,O)=>({word:zZ(D.text),redirect:Y.redirects.has(O)})),U=B.slice(0,H<0?B.length:H).map((D)=>D.word),V=H>=0?a(B[H].word.value):"";if(H<0||["export","declare","typeset","readonly","local"].includes(V)){if(Q=Z1([...U,...B.slice(H+1).map((D)=>D.word)],Q),H<0)continue}let I=Z1(U,Q),j=B.slice(H+1).filter((D)=>!D.redirect).map((D)=>D.word),P=B.slice(H+1).filter((D)=>D.redirect).map((D)=>D.word),M=B[H].word,_=a(M.value),g={depth:J,appendsArgs:!1,debug:I};if(H>0&&B[H-1].word.value==="function"){let D=j.filter((v)=>!["{","()","(",")"].includes(v.value)),O=U0(D,g);if(O.refused)return O;continue}if(G>0&&X1(Y.sepBefore)&&H0(_,j)){let D=X[G-1],O=H3(Z,D),v=O===null?null:B0(O);if(v!==null){let S=x(v,J+1,I);if(S.refused)return S}}let h=X[G+1];if(h&&/<\(\s*$/.test(h.sepBefore)&&(wZ.has(_)||_==="source"||_===".")){let D=h.words[0].start-h.sepBefore.length+h.sepBefore.lastIndexOf("("),O=_Z(Z,D),v=O<0?null:B0(Z.slice(D+1,O-1));if(v!==null){let S=x(v,J+1,I);if(S.refused)return S}}let d=z1(_,M,j,P,Y.words.slice(0,H+1),g);if(d.refused)return d}return N}function H3(Z,J){let $=J.words[0],X=J.words[J.words.length-1];return $&&X?Z.slice($.start,X.end):null}function B3(Z){let J=a(Z.value);return/[$`]/.test(J)||J===""?void 0:J}function z1(Z,J,$,X,z,Q){let K=Q.depth,G=(Y)=>x(Y,K+1,Q.debug);if(J.opaque){let Y=B3(J);if(Y===void 0){let H=$.find((U)=>!U.value.startsWith("-")),B=fZ($,Q);if(!B.refused)return N;return B.rule!=="unclassifiable"||H!==void 0&&!H.opaque&&x2.has(H.value)?B:N}Z=Y}if(Z==="gh")return fZ($,Q);if(wZ.has(Z)){let Y=q0($);if(Y){let H=G(Y.value);if(H.refused||!Y.opaque)return H;return Q0(Y.raw,K,Q)}for(let H of X){let B=G(H.value);if(B.refused)return B;if(H.opaque){let U=Q0(H.raw,K,Q);if(U.refused)return U}}return N}if(J3.has(Z)){let Y=$.findIndex((B)=>B.value==="-c"||B.value==="--command"||/^-[A-Za-z]*c$/.test(B.value)),H=Y>=0?$[Y+1]:void 0;return H?G(H.value):K0($,Q)}if(Z==="eval"||Z==="watch"){let Y=$;if(Z==="watch"){let B=0;while(B<$.length&&$[B].value.startsWith("-")){if($[B].value==="--"){B++;break}if(["-n","--interval","-q","--equexit"].includes($[B].value))B++;B++}Y=$.slice(B)}let H=G(Y.map((B)=>B.value).join(" "));if(H.refused)return H;for(let B of Y){let U=Q0(B.raw,K,Q);if(U.refused)return U}return N}if(Z==="env")return G(qZ([...z.map((Y)=>zZ(Y.text).value),...$.map((Y)=>Y.value)]));if(Z==="ssh"){let Y=0;for(;Y<$.length;Y++){let U=$[Y].value;if(!U.startsWith("-"))break;if(Q3.has(U))Y++}let H=$.slice(Y+1);if(H[0]?.value==="--")H.shift();let B=H.map((U)=>U.value).join(" ");return B?G(B):N}if(Z==="xargs"){let Y=0,H=!1;for(;Y<$.length;Y++){let B=$[Y].value;if(B==="--"){Y++;break}if(!B.startsWith("-"))break;if(/^-I|^-i|^--replace/.test(B))H=!0;if(Y3.has(B))Y++}return U0($.slice(Y),{depth:K,appendsArgs:!H,debug:Q.debug})}if(Z==="find"){for(let Y=0;Y<$.length;Y++){if(!["-exec","-execdir","-ok","-okdir"].includes($[Y].value))continue;let H=Y+1;while(H<$.length&&$[H].value!==";"&&$[H].value!=="+")H++;let B=U0($.slice(Y+1,H),Q);if(B.refused)return B;Y=H}return N}if(X3.has(Z))return K0($,Q);if(z3.get(Z)?.includes($[0]?.value??""))return K0($.slice(1),Q);return N}function U0(Z,J){if(Z.length===0)return N;let $=a(Z[0].value);if($==="gh")return fZ(Z.slice(1),J);if(wZ.has($)){let X=q0(Z.slice(1));return X?x(X.value,J.depth+1,J.debug):N}return z1($,Z[0],Z.slice(1),[],[],J)}function K0(Z,J){let $=Z.findIndex((X)=>!X.opaque&&a(X.value)==="gh"&&(X.value==="gh"||X.value.includes("/")));return $>=0?fZ(Z.slice($+1),J):N}function q0(Z){for(let J=0;J<Z.length;J++){let $=Z[J].value;if($==="--")return;if(!$.startsWith("-")&&!$.startsWith("+"))return;if($==="-c"||/^-[A-Za-z]+$/.test($)&&$.includes("c"))return Z.slice(J+1).find((X)=>!X.value.startsWith("-")||X.value==="-");if(/^[-+][A-Za-z]*[oO]$/.test($))J++}return}function U3(Z){let J=[],$=0;for(;;){let X=Z.indexOf("<<",$);if(X<0)break;if($=X+2,Z[X+2]==="<"||X>0&&Z[X-1]==="<"){$=X+3;continue}let z=EZ(Z,X);if(!z)continue;let Q=Z.lastIndexOf(`
|
|
15
|
+
`,X)+1,K=Z.indexOf(`
|
|
16
|
+
`,X);if(K<0)continue;let G=RZ(Z,K,[z]),Y=G<0?Z.slice(K+1):Z.slice(K+1,G),H=G<0?Y:Y.slice(0,Math.max(0,Y.lastIndexOf(z.delimiter))).replace(/\n$/,""),B=Z.slice(Q,X)+" HEREDOC_OPERATOR "+Z.slice(z.end,K);J.push({body:H,runsBody:V3(B),quoted:z.quoted})}return J}function V3(Z){let{segments:J}=N0(Z),$=J.findIndex((G)=>G.words.some((Y)=>Y.text==="HEREDOC_OPERATOR"));if($<0)return!1;let X=(G)=>{let Y=u(G);if(Y<0)return{verb:"",args:[]};let H=G.words.slice(Y+1).filter((B)=>B.text!=="HEREDOC_OPERATOR").map((B)=>zZ(B.text));return{verb:a(zZ(G.words[Y].text).value),args:H}},{verb:z,args:Q}=X(J[$]);if(z==="ssh"||H0(z,Q))return!0;if((z==="source"||z===".")&&["/dev/stdin","/dev/fd/0","/proc/self/fd/0"].includes(Q[0]?.value??""))return!0;let K=J[$+1];if(z==="cat"&&K&&X1(K.sepBefore)){let G=X(K);return G.verb==="ssh"||H0(G.verb,G.args)}return!1}var M3=/(?:^|[^A-Za-z0-9_.-])gh(?:$|[^A-Za-z0-9_.-])/;function Y1(){return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:V0}}}var F3=new Set(["Bash","Monitor"]);function Q1(Z){if(Z?.hook_event_name!=="PreToolUse"||!F3.has(Z.tool_name??""))return{continue:!0};let J=e(Z);if(!J.trim())return{continue:!0};try{return x(J).refused?Y1():{continue:!0}}catch{return M3.test(J)?Y1():{continue:!0}}}var _0=process.argv[1];if(_0!=="trash-guard"&&_0!=="workspace-repos-guard")throw Error("Invalid safety capability");var W=JSON.parse(await Bun.stdin.text());if(!W||Array.isArray(W)||W.hook_event_name!=="PreToolUse"||typeof W.tool_name!=="string"||!W.tool_input||typeof W.tool_input!=="object"||Array.isArray(W.tool_input)||typeof W.cwd!=="string"||!B1(W.cwd))throw Error("Invalid safety input");if(W.tool_name==="Bash"&&typeof W.tool_input.command!=="string")throw Error("Invalid shell input");if(W.tool_name==="Monitor"&&W.tool_input.command!==void 0&&typeof W.tool_input.command!=="string")throw Error("Invalid monitor input");var p=W.hasna_execution;if(p!==void 0&&(!p||Object.keys(p).sort().join()!=="agent,schema"||p.schema!=="hasna.hooks.execution.v1"||typeof p.agent!=="string"||!/^[A-Za-z0-9][A-Za-z0-9._:@-]{0,127}$/.test(p.agent)))throw Error("Invalid execution capability");var xZ=p===void 0?void 0:{runtime:process.execPath,home:T0(),path:process.env.PATH,agent:p.agent};if(xZ&&(!B1(xZ.runtime)||!xZ.path))throw Error("Invalid bound execution context");if(["apply_patch","ApplyPatch","functions.apply_patch"].includes(W.tool_name)){if(W.tool_input.command!==void 0&&W.tool_input.patch!==void 0&&W.tool_input.command!==W.tool_input.patch)throw Error("Conflicting patch aliases");let Z=W.tool_input.command??W.tool_input.patch;if(typeof Z!=="string")throw Error("Invalid patch input");W.tool_input={...W.tool_input,command:Z,patch:Z}}var gZ=W.tool_name==="Monitor",C0=W.tool_name==="Bash"||gZ&&typeof W.tool_input.command==="string",hZ=gZ&&C0?{...W,tool_name:"Bash"}:W,j0=(Z)=>({hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:Z}}),K1=c0(hZ).output,G1=Q1(hZ),N3=C0&&jZ(hZ.tool_input.command,{home:T0(),cwd:W.cwd}).handoffHits.length>0,W0=_0==="trash-guard"&&(!gZ||C0)?f0(hZ,{home:T0(),cwd:W.cwd,findTrash:OZ,inspectTrash:VZ,execution:xZ}):{continue:!0};if(gZ&&W0.hookSpecificOutput?.permissionDecision==="allow")W0=j0("[trash-guard] A deletion inside a Monitor command is refused; run it with the Bash tool, where it is redirected into trash.");var H1=K1.decision==="block"?j0(K1.reason):G1.hookSpecificOutput?G1:N3?j0("[workspace-repos-guard] Deletion under a protected repository checkout is refused."):W0;process.stdout.write(JSON.stringify({verdict:"continue"in H1?null:H1}));
|