@pushary/agent-hooks 0.49.1 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,595 @@
1
+ // ../contracts/src/index.ts
2
+ var APPROVAL_MODES = ["push_only", "terminal_only", "push_first", "notify_only"];
3
+ var isApprovalMode = (value) => typeof value === "string" && APPROVAL_MODES.includes(value);
4
+ var HOOK_BUDGETS = {
5
+ claude: { budgetSeconds: 120, guardSeconds: 10 },
6
+ codex: { budgetSeconds: 180, guardSeconds: 10 },
7
+ gemini: { budgetSeconds: 180, guardSeconds: 10 },
8
+ // The Cursor gate is a dependency-free .mjs that cannot import this module; it
9
+ // mirrors these as MAX_BLOCK_MS (45s) inside its 60s failClosed budget.
10
+ cursor: { budgetSeconds: 60, guardSeconds: 15 }
11
+ };
12
+ var hookMaxWaitSeconds = (agent) => Math.max(HOOK_BUDGETS[agent].budgetSeconds - HOOK_BUDGETS[agent].guardSeconds, 1);
13
+ var hookWaitDeadline = (startMs, policyWaitSeconds, agent, nowMs) => {
14
+ const maxWait = hookMaxWaitSeconds(agent);
15
+ return Math.min(
16
+ nowMs + Math.min(Math.max(policyWaitSeconds, 0), maxWait) * 1e3,
17
+ startMs + maxWait * 1e3
18
+ );
19
+ };
20
+ var hookWaitClamped = (startMs, policyWaitSeconds, agent, nowMs) => startMs + hookMaxWaitSeconds(agent) * 1e3 < nowMs + Math.max(policyWaitSeconds, 0) * 1e3;
21
+ var effectiveWaitSeconds = (timeoutAction, policySeconds, agent) => timeoutAction === "wait" ? hookMaxWaitSeconds(agent) : policySeconds;
22
+ var MATCH_RANKS = ["none", "tool", "prefix", "exact"];
23
+ var matchRankWeight = (rank) => MATCH_RANKS.indexOf(rank);
24
+ var matchToolPattern = (pattern, toolName, arg) => {
25
+ const open = pattern.indexOf("(");
26
+ if (open === -1 || !pattern.endsWith(")")) {
27
+ return pattern === toolName ? "tool" : "none";
28
+ }
29
+ if (pattern.slice(0, open) !== toolName || arg === void 0) return "none";
30
+ const inner = pattern.slice(open + 1, -1);
31
+ if (inner.endsWith(":*")) {
32
+ return arg.startsWith(inner.slice(0, -2)) ? "prefix" : "none";
33
+ }
34
+ return arg === inner ? "exact" : "none";
35
+ };
36
+ var POLICY_ARG_KEYS = {
37
+ Bash: "command",
38
+ Edit: "file_path",
39
+ Write: "file_path"
40
+ };
41
+ var extractPolicyArg = (toolName, toolInput) => {
42
+ const key = POLICY_ARG_KEYS[toolName];
43
+ if (!key) return void 0;
44
+ const value = toolInput[key];
45
+ return typeof value === "string" ? value : void 0;
46
+ };
47
+ var SAFE_SHELL_COMMANDS = /* @__PURE__ */ new Set([
48
+ "ls",
49
+ "pwd",
50
+ "cd",
51
+ "cat",
52
+ "head",
53
+ "tail",
54
+ "wc",
55
+ "echo",
56
+ "printf",
57
+ "which",
58
+ "type",
59
+ "whoami",
60
+ "id",
61
+ "uname",
62
+ "arch",
63
+ "printenv",
64
+ "locale",
65
+ "tty",
66
+ "dirname",
67
+ "basename",
68
+ "realpath",
69
+ "readlink",
70
+ "stat",
71
+ "cut",
72
+ "nl",
73
+ "tr",
74
+ "comm",
75
+ "diff",
76
+ "cmp",
77
+ "grep",
78
+ "egrep",
79
+ "fgrep",
80
+ "jq",
81
+ "cksum",
82
+ "md5sum",
83
+ "sha1sum",
84
+ "sha256sum",
85
+ "du",
86
+ "df",
87
+ "ps",
88
+ "true"
89
+ ]);
90
+ var SAFE_GIT_SUBCOMMANDS = /* @__PURE__ */ new Set([
91
+ "status",
92
+ "log",
93
+ "diff",
94
+ "show",
95
+ "rev-parse",
96
+ "describe",
97
+ "blame",
98
+ "shortlog",
99
+ "ls-files",
100
+ "ls-tree",
101
+ "cat-file",
102
+ "whatchanged",
103
+ "rev-list",
104
+ "name-rev",
105
+ "for-each-ref",
106
+ "var",
107
+ "count-objects"
108
+ ]);
109
+ var GIT_WRITE_FLAGS = (token) => token === "--output" || token.startsWith("--output=");
110
+ var DANGEROUS_FIND_ACTIONS = /* @__PURE__ */ new Set([
111
+ "-exec",
112
+ "-execdir",
113
+ "-ok",
114
+ "-okdir",
115
+ "-delete",
116
+ "-fprintf",
117
+ "-fprint",
118
+ "-fprint0",
119
+ "-fls"
120
+ ]);
121
+ var SAFE_FIND_TOKENS = /* @__PURE__ */ new Set([
122
+ "-name",
123
+ "-iname",
124
+ "-path",
125
+ "-ipath",
126
+ "-wholename",
127
+ "-iwholename",
128
+ "-lname",
129
+ "-ilname",
130
+ "-regex",
131
+ "-iregex",
132
+ "-type",
133
+ "-xtype",
134
+ "-maxdepth",
135
+ "-mindepth",
136
+ "-depth",
137
+ "-mount",
138
+ "-xdev",
139
+ "-size",
140
+ "-empty",
141
+ "-perm",
142
+ "-readable",
143
+ "-writable",
144
+ "-executable",
145
+ "-mtime",
146
+ "-mmin",
147
+ "-atime",
148
+ "-amin",
149
+ "-ctime",
150
+ "-cmin",
151
+ "-newer",
152
+ "-newermt",
153
+ "-anewer",
154
+ "-cnewer",
155
+ "-user",
156
+ "-uid",
157
+ "-group",
158
+ "-gid",
159
+ "-nouser",
160
+ "-nogroup",
161
+ "-samefile",
162
+ "-inum",
163
+ "-links",
164
+ "-print",
165
+ "-print0",
166
+ "-printf",
167
+ "-ls",
168
+ "-quit",
169
+ "-prune",
170
+ "-follow",
171
+ "-noleaf",
172
+ "-ignore_readdir_race",
173
+ "-noignore_readdir_race",
174
+ "-true",
175
+ "-false",
176
+ "-not",
177
+ "-and",
178
+ "-or",
179
+ "-o",
180
+ "-a",
181
+ "-P",
182
+ "-L",
183
+ "-H",
184
+ "-D",
185
+ "-O"
186
+ ]);
187
+ var SAFE_REDIRECTION = /\s*(?:[0-9]*>&[0-9-]+|(?:[0-9]*|&)>>?\s*\/dev\/null)/g;
188
+ var basenameOf = (token) => {
189
+ const slash = Math.max(token.lastIndexOf("/"), token.lastIndexOf("\\"));
190
+ return slash === -1 ? token : token.slice(slash + 1);
191
+ };
192
+ var TRUSTED_EXE_PREFIXES = [
193
+ "/bin/",
194
+ "/sbin/",
195
+ "/usr/bin/",
196
+ "/usr/sbin/",
197
+ "/usr/local/bin/",
198
+ "/opt/homebrew/bin/"
199
+ ];
200
+ var isTrustedExecutable = (first) => {
201
+ if (!first.includes("/")) return true;
202
+ if (first.includes("..")) return false;
203
+ return TRUSTED_EXE_PREFIXES.some((prefix) => first.startsWith(prefix));
204
+ };
205
+ var tokenizeShellCommand = (command) => {
206
+ const tokens = [];
207
+ let current = "";
208
+ let quote = null;
209
+ let started = false;
210
+ for (const ch of command) {
211
+ if (quote) {
212
+ if (ch === quote) quote = null;
213
+ else current += ch;
214
+ started = true;
215
+ continue;
216
+ }
217
+ if (ch === '"' || ch === "'") {
218
+ quote = ch;
219
+ started = true;
220
+ continue;
221
+ }
222
+ if (ch === " " || ch === " ") {
223
+ if (started) {
224
+ tokens.push(current);
225
+ current = "";
226
+ started = false;
227
+ }
228
+ continue;
229
+ }
230
+ current += ch;
231
+ started = true;
232
+ }
233
+ if (quote) return null;
234
+ if (started) tokens.push(current);
235
+ return tokens;
236
+ };
237
+ var isSafeSimpleCommand = (segment) => {
238
+ const tokens = tokenizeShellCommand(segment.trim());
239
+ if (!tokens || tokens.length === 0) return false;
240
+ const first = tokens[0];
241
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(first) || first.includes("$")) return false;
242
+ if (!isTrustedExecutable(first)) return false;
243
+ const exe = basenameOf(first);
244
+ if (exe === "git") {
245
+ const sub = tokens[1];
246
+ if (!sub || sub.startsWith("-")) return false;
247
+ if (!SAFE_GIT_SUBCOMMANDS.has(sub)) return false;
248
+ return !tokens.some(GIT_WRITE_FLAGS);
249
+ }
250
+ if (exe === "find") {
251
+ for (let i = 1; i < tokens.length; i += 1) {
252
+ const t = tokens[i];
253
+ if (t === "(" || t === ")" || t === "!" || t === "\\(" || t === "\\)" || t === "\\!") continue;
254
+ if (t.startsWith("-") && (DANGEROUS_FIND_ACTIONS.has(t) || !SAFE_FIND_TOKENS.has(t))) return false;
255
+ }
256
+ return true;
257
+ }
258
+ return SAFE_SHELL_COMMANDS.has(exe);
259
+ };
260
+ var isSafeReadOnlyCommand = (command) => {
261
+ const trimmed = command.trim();
262
+ if (!trimmed || trimmed.length > 2e3) return false;
263
+ if (/[`\n\r{}]/.test(trimmed)) return false;
264
+ if (/;|\|\|/.test(trimmed)) return false;
265
+ if (/\\(?![()!])/.test(trimmed)) return false;
266
+ if (/(?<!\\)[()]/.test(trimmed)) return false;
267
+ const noRedir = trimmed.replace(SAFE_REDIRECTION, " ");
268
+ if (/[<>]/.test(noRedir)) return false;
269
+ if (noRedir.replace(/&&/g, " ").includes("&")) return false;
270
+ for (const andPart of noRedir.split("&&")) {
271
+ if (!andPart.trim()) return false;
272
+ for (const segment of andPart.split("|")) {
273
+ if (!isSafeSimpleCommand(segment)) return false;
274
+ }
275
+ }
276
+ return true;
277
+ };
278
+ var API_KEY_PATTERN = /^pk_[a-f0-9]+\.[a-f0-9]+$/;
279
+ var isValidApiKey = (value) => API_KEY_PATTERN.test(value);
280
+ var ACTION_BODY_MAX = 4e3;
281
+ var DECISION_LINE_MAX = 500;
282
+ var APPROVAL_FATIGUE_FLAG_PATTERNS = [
283
+ /\brm\s+-[a-z]*r/i,
284
+ /\brmdir\b/i,
285
+ /git\s+push[^\n]*(--force|--force-with-lease|\s-f\b)/i,
286
+ /git\s+reset\s+--hard/i,
287
+ /git\s+clean\s+-[a-z]*f/i,
288
+ /\bdrop\s+(table|database|schema)\b/i,
289
+ /\bdelete\s+from\b/i,
290
+ /\btruncate\b/i,
291
+ /\b(deploy|publish|release)\b/i,
292
+ /\bnpm\s+publish\b/i,
293
+ /\bsudo\b/i,
294
+ /chmod\s+-?R?\s*777/i,
295
+ /\bkubectl\s+delete\b/i,
296
+ /\bterraform\s+(apply|destroy)\b/i,
297
+ /\bdocker\s+system\s+prune\b/i
298
+ ];
299
+ var DELTA_STOPWORDS = /* @__PURE__ */ new Set([
300
+ "the",
301
+ "a",
302
+ "an",
303
+ "to",
304
+ "of",
305
+ "for",
306
+ "and",
307
+ "is",
308
+ "in",
309
+ "on",
310
+ "with"
311
+ ]);
312
+ var HIGH_CONSEQUENCE_PATTERNS = [
313
+ /(^|[\\/])\.env(\.|$|[\\/])/i,
314
+ /secrets?/i,
315
+ /credentials?/i,
316
+ /(^|[\\/])deploy/i,
317
+ /\.github[\\/]workflows/i,
318
+ /package\.json/i
319
+ ];
320
+ var isCommandTool = (toolName) => {
321
+ const name = (toolName ?? "").toLowerCase();
322
+ return name === "bash" || name === "shell" || name === "run_shell_command" || name === "apply_patch" || name.includes("command");
323
+ };
324
+ var normalizeTarget = (toolName, toolTarget, actionBody) => {
325
+ const command = isCommandTool(toolName);
326
+ const target = toolTarget ?? "";
327
+ const body = actionBody ?? "";
328
+ const commandText = command ? body.length > 0 ? body : target : `${target}
329
+ ${body}`;
330
+ const path = command ? "" : target;
331
+ const scanText = command ? commandText : `${path}
332
+ ${body}`;
333
+ const highConsequence = HIGH_CONSEQUENCE_PATTERNS.some((re) => re.test(scanText));
334
+ return { isCommand: command, commandText, path, highConsequence };
335
+ };
336
+ var isDestructive = (toolName, commandText) => {
337
+ const haystack = `${toolName ?? ""}
338
+ ${commandText}`;
339
+ return APPROVAL_FATIGUE_FLAG_PATTERNS.some((re) => re.test(haystack));
340
+ };
341
+ var addAlnumTokens = (source, set) => {
342
+ for (const raw of source.toLowerCase().split(/[^a-z0-9]+/)) {
343
+ if (raw) set.add(raw);
344
+ }
345
+ };
346
+ var tokenize = (text) => {
347
+ const set = /* @__PURE__ */ new Set();
348
+ for (const raw of text.toLowerCase().split(/[^a-z0-9]+/)) {
349
+ if (raw && !DELTA_STOPWORDS.has(raw)) set.add(raw);
350
+ }
351
+ return set;
352
+ };
353
+ var targetTokenSet = (target) => {
354
+ const set = /* @__PURE__ */ new Set();
355
+ if (target.isCommand) {
356
+ const parts = target.commandText.trim().split(/\s+/).filter(Boolean);
357
+ if (parts[0]) addAlnumTokens(basenameOf(parts[0]), set);
358
+ if (parts[1]) addAlnumTokens(basenameOf(parts[1]), set);
359
+ } else {
360
+ addAlnumTokens(target.path, set);
361
+ }
362
+ return set;
363
+ };
364
+ var intersectionSize = (a, b) => {
365
+ const [small, large] = a.size <= b.size ? [a, b] : [b, a];
366
+ let count = 0;
367
+ for (const token of small) if (large.has(token)) count += 1;
368
+ return count;
369
+ };
370
+ var computeDeltaSignal = (input) => {
371
+ const target = normalizeTarget(input.toolName, input.toolTarget, input.actionBody);
372
+ const destructive = isDestructive(input.toolName, target.commandText);
373
+ if (!input.intent || input.intent.trim().length === 0) {
374
+ return { deltaSignal: "unclear", deltaScore: 0, remoteApprove: !destructive };
375
+ }
376
+ const intentTokens = tokenize(input.intent);
377
+ const targetTokens = targetTokenSet(target);
378
+ const overlap = intersectionSize(intentTokens, targetTokens);
379
+ const zeroOverlap = overlap === 0 && targetTokens.size > 0;
380
+ if (zeroOverlap && (destructive || target.highConsequence)) {
381
+ return { deltaSignal: "mismatch", deltaScore: 1, remoteApprove: false };
382
+ }
383
+ if (zeroOverlap) {
384
+ return { deltaSignal: "unclear", deltaScore: 0.5, remoteApprove: true };
385
+ }
386
+ return { deltaSignal: "match", deltaScore: 0, remoteApprove: !destructive };
387
+ };
388
+ var SECRET_REDACTION_RULES = [
389
+ { pattern: /-----BEGIN[A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z0-9 ]*PRIVATE KEY-----/g, replacement: "[redacted key]" },
390
+ { pattern: /\bsk-[A-Za-z0-9_-]{16,}\b/g, replacement: "[redacted]" },
391
+ { pattern: /\b[spr]k_(?:live|test)_[A-Za-z0-9]{8,}\b/g, replacement: "[redacted]" },
392
+ { pattern: /\bwhsec_[A-Za-z0-9]{16,}\b/g, replacement: "[redacted]" },
393
+ { pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b/g, replacement: "[redacted]" },
394
+ { pattern: /\bgithub_pat_[A-Za-z0-9_]{22,}\b/g, replacement: "[redacted]" },
395
+ { pattern: /\bglpat-[A-Za-z0-9_-]{20,}\b/g, replacement: "[redacted]" },
396
+ { pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, replacement: "[redacted]" },
397
+ { pattern: /\bAIza[A-Za-z0-9_-]{35}\b/g, replacement: "[redacted]" },
398
+ { pattern: /\bAKIA[0-9A-Z]{16}\b/g, replacement: "[redacted]" },
399
+ { pattern: /\bnpm_[A-Za-z0-9]{36}\b/g, replacement: "[redacted]" },
400
+ { pattern: /\bxai-[A-Za-z0-9]{16,}\b/g, replacement: "[redacted]" },
401
+ { pattern: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, replacement: "[redacted]" },
402
+ { pattern: /\bbearer\s+[A-Za-z0-9._~+/=-]+/gi, replacement: "bearer [redacted]" },
403
+ { pattern: /\bauthorization:\s*\S+/gi, replacement: "authorization: [redacted]" },
404
+ {
405
+ pattern: /((?:secret|token|password|passwd|api[_-]?key|access[_-]?key|client[_-]?secret|private[_-]?key)\s*[=:]\s*)("[^"]*"|'[^']*'|\S+)/gi,
406
+ replacement: "$1[redacted]"
407
+ }
408
+ ];
409
+ var HIGH_ENTROPY_RULE = { pattern: /[A-Za-z0-9+/]{40,}={0,2}/g, replacement: "[redacted]" };
410
+ var redactSecrets = (text) => SECRET_REDACTION_RULES.reduce((acc, rule) => acc.replace(rule.pattern, rule.replacement), text);
411
+ var redactSecretsDeep = (text) => redactSecrets(text).replace(HIGH_ENTROPY_RULE.pattern, HIGH_ENTROPY_RULE.replacement);
412
+ var classifyDecisionRisk = (toolName, toolTarget) => {
413
+ const parts = [toolName, toolTarget ?? ""];
414
+ const matchesAny = (patterns) => parts.some((part) => patterns.some((re) => re.test(part)));
415
+ if (matchesAny(APPROVAL_FATIGUE_FLAG_PATTERNS)) return "destructive";
416
+ if (matchesAny(HIGH_CONSEQUENCE_PATTERNS)) return "elevated";
417
+ return "standard";
418
+ };
419
+ var POLICY_CAPABILITIES = ["read", "edit", "shell", "network", "destructive"];
420
+ var CAPABILITY_PATTERNS = {
421
+ read: ["Read", "Grep", "Glob", "LS", "NotebookRead"],
422
+ edit: ["Edit", "Write"],
423
+ shell: ["Bash"],
424
+ network: [
425
+ "WebFetch",
426
+ "Bash(curl:*)",
427
+ "Bash(wget:*)",
428
+ "Bash(nc:*)",
429
+ "Bash(ssh:*)",
430
+ "Bash(scp:*)",
431
+ "Bash(npm install:*)",
432
+ "Bash(npm i:*)",
433
+ "Bash(pnpm add:*)",
434
+ "Bash(yarn add:*)",
435
+ "Bash(bun add:*)",
436
+ "Bash(pip install:*)",
437
+ "Bash(brew install:*)",
438
+ "Bash(apt:*)",
439
+ "Bash(apt-get:*)"
440
+ ],
441
+ destructive: [
442
+ "Bash(rm:*)",
443
+ "Bash(rmdir:*)",
444
+ "Bash(sudo:*)",
445
+ "Bash(git push:*)",
446
+ "Bash(git reset --hard:*)",
447
+ "Bash(git clean:*)",
448
+ "Bash(npm publish:*)",
449
+ "Bash(kubectl delete:*)",
450
+ "Bash(terraform apply:*)",
451
+ "Bash(terraform destroy:*)",
452
+ "Bash(docker system prune:*)",
453
+ "Bash(dropdb:*)",
454
+ "Bash(mkfs:*)"
455
+ ]
456
+ };
457
+ var DECISION_EPISODE_SCHEMA_VERSION = 1;
458
+ var DECISION_FEATURE_EXTRACTOR = `pushary-hooks/schema-${DECISION_EPISODE_SCHEMA_VERSION}`;
459
+ var EPISODE_CAPABILITIES = [...POLICY_CAPABILITIES, "other"];
460
+ var NETWORK_COMMAND_PATTERNS = [
461
+ /\bcurl\b/i,
462
+ /\bwget\b/i,
463
+ /\bnc\b/i,
464
+ /\bssh\b/i,
465
+ /\bscp\b/i,
466
+ /\b(?:npm|pnpm|yarn|bun)\s+(?:install|i|add)\b/i,
467
+ /\bpip\s+install\b/i,
468
+ /\bbrew\s+install\b/i,
469
+ /\bapt(?:-get)?\s+(?:install|update|upgrade)\b/i
470
+ ];
471
+ var SECRET_TARGET_PATTERNS = [
472
+ /(^|[\\/])\.env(\.|$|[\\/])/i,
473
+ /secrets?/i,
474
+ /credentials?/i,
475
+ /private[_-]?key/i,
476
+ /\.pem$/i
477
+ ];
478
+ var READ_TOOL_NAMES = new Set(CAPABILITY_PATTERNS.read);
479
+ var EDIT_TOOL_NAMES = new Set(CAPABILITY_PATTERNS.edit);
480
+ var deriveEpisodeCapability = (toolName, commandText) => {
481
+ if (READ_TOOL_NAMES.has(toolName)) return "read";
482
+ if (EDIT_TOOL_NAMES.has(toolName)) return "edit";
483
+ if (toolName === "WebFetch" || toolName === "WebSearch") return "network";
484
+ if (isCommandTool(toolName)) {
485
+ if (isDestructive(toolName, commandText)) return "destructive";
486
+ if (NETWORK_COMMAND_PATTERNS.some((re) => re.test(commandText))) return "network";
487
+ return "shell";
488
+ }
489
+ if (isDestructive(toolName, commandText)) return "destructive";
490
+ return "other";
491
+ };
492
+ var deriveTargetClass = (path) => {
493
+ const p = path.toLowerCase();
494
+ if (!p) return "other";
495
+ if (SECRET_TARGET_PATTERNS.some((re) => re.test(p))) return "secret";
496
+ if (/(^|[\\/])(?:tests?|__tests__|spec)([\\/]|$)|\.(?:test|spec)\.[a-z0-9]+$/i.test(p)) return "test";
497
+ if (/\.(?:md|mdx|txt|rst)$|(^|[\\/])docs?([\\/]|$)/i.test(p)) return "docs";
498
+ if (/(^|[\\/])(?:package\.json|tsconfig[^\\/]*\.json|\.[a-z]+rc)$|\.(?:json|ya?ml|toml|ini|conf|cfg)$/i.test(p)) return "config";
499
+ if (/\.(?:ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|rb|php|c|cc|cpp|h|hpp|cs|swift|kt|scala|sh)$/i.test(p)) return "source";
500
+ return "other";
501
+ };
502
+ var bucketArgCount = (n) => n <= 0 ? "none" : n <= 3 ? "few" : "many";
503
+ var bucketLength = (n) => n < 40 ? "short" : n < 200 ? "medium" : "long";
504
+ var deriveArgShape = (isCommand, commandText, path) => {
505
+ const text = isCommand ? commandText : path;
506
+ const tokenCount = isCommand ? text.trim().split(/\s+/).filter(Boolean).length : 0;
507
+ return {
508
+ hasPipe: isCommand && /(?<!\|)\|(?!\|)/.test(text),
509
+ hasRedirect: isCommand && /(?<![0-9])[<>]/.test(text),
510
+ hasChain: isCommand && /&&|\|\||;/.test(text),
511
+ hasSudo: /\bsudo\b/i.test(text),
512
+ hasGlob: isCommand && /[*?]/.test(text),
513
+ hasSubshell: isCommand && /\$\(|`/.test(text),
514
+ touchesSecretPath: SECRET_TARGET_PATTERNS.some((re) => re.test(text)),
515
+ // The command word itself is not an argument, so subtract one.
516
+ argCountBucket: bucketArgCount(isCommand ? Math.max(0, tokenCount - 1) : 0),
517
+ lengthBucket: bucketLength(text.length)
518
+ };
519
+ };
520
+ var deriveReversibility = (capability, riskClass) => {
521
+ if (capability === "read") return "none";
522
+ if (capability === "destructive" || riskClass === "destructive") return "irreversible";
523
+ return "reversible";
524
+ };
525
+ var deriveBlastRadius = (capability, riskClass, targetClass) => {
526
+ if (capability === "read") return "low";
527
+ if (capability === "destructive" || riskClass === "destructive" || targetClass === "secret") return "high";
528
+ if (capability === "network" || capability === "shell" || riskClass === "elevated" || targetClass === "config") return "medium";
529
+ return "low";
530
+ };
531
+ var deriveLabelTier = (origin, decision) => {
532
+ if (origin === "autopilot") return "none";
533
+ if (origin === "human" && (decision === "approve" || decision === "deny")) return "bronze";
534
+ return "bronze";
535
+ };
536
+ var buildDecisionEpisodeFeatures = (input) => {
537
+ const isCommand = isCommandTool(input.toolName);
538
+ const raw = input.commandOrPath ?? input.toolTarget ?? "";
539
+ const commandText = isCommand ? raw : "";
540
+ const path = isCommand ? "" : raw;
541
+ const capability = deriveEpisodeCapability(input.toolName, commandText);
542
+ const riskClass = classifyDecisionRisk(input.toolName, input.toolTarget ?? raw);
543
+ const targetClass = isCommand ? SECRET_TARGET_PATTERNS.some((re) => re.test(commandText)) ? "secret" : "other" : deriveTargetClass(path);
544
+ const argShape = deriveArgShape(isCommand, commandText, path);
545
+ const { deltaSignal } = computeDeltaSignal({
546
+ intent: input.intent,
547
+ toolName: input.toolName,
548
+ toolTarget: input.toolTarget,
549
+ actionBody: input.commandOrPath
550
+ });
551
+ return {
552
+ schemaVersion: DECISION_EPISODE_SCHEMA_VERSION,
553
+ extractorVersion: DECISION_FEATURE_EXTRACTOR,
554
+ capability,
555
+ toolFamily: input.toolName.trim().slice(0, 40),
556
+ riskClass,
557
+ targetClass,
558
+ argShape,
559
+ reversibility: deriveReversibility(capability, riskClass),
560
+ blastRadius: deriveBlastRadius(capability, riskClass, targetClass),
561
+ deltaSignal,
562
+ preset: input.preset,
563
+ // A policy artifact (usually canonical like `Bash(rm:*)`), but a user could
564
+ // author a custom pattern, so scrub secrets defensively before it is stored.
565
+ matchedPattern: input.matchedPattern ? redactSecretsDeep(input.matchedPattern).slice(0, 120) : void 0,
566
+ policyMode: input.policyMode,
567
+ requestedGate: input.requestedGate,
568
+ timeoutSeconds: input.timeoutSeconds,
569
+ timeoutAction: input.timeoutAction,
570
+ decision: input.decision,
571
+ origin: input.origin,
572
+ latencyMs: input.latencyMs,
573
+ labelTier: deriveLabelTier(input.origin, input.decision),
574
+ agentType: input.agentType
575
+ };
576
+ };
577
+
578
+ export {
579
+ isApprovalMode,
580
+ HOOK_BUDGETS,
581
+ hookWaitDeadline,
582
+ hookWaitClamped,
583
+ effectiveWaitSeconds,
584
+ matchRankWeight,
585
+ matchToolPattern,
586
+ extractPolicyArg,
587
+ isSafeReadOnlyCommand,
588
+ isValidApiKey,
589
+ ACTION_BODY_MAX,
590
+ DECISION_LINE_MAX,
591
+ redactSecrets,
592
+ redactSecretsDeep,
593
+ classifyDecisionRisk,
594
+ buildDecisionEpisodeFeatures
595
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HOOK_BUDGETS
3
- } from "./chunk-Z5PL3K7C.js";
3
+ } from "./chunk-CJBVT33U.js";
4
4
 
5
5
  // src/claude-config.ts
6
6
  import { join } from "path";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  isSafeReadOnlyCommand
3
- } from "./chunk-Z5PL3K7C.js";
3
+ } from "./chunk-CJBVT33U.js";
4
4
 
5
5
  // src/ledger.ts
6
6
  import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync } from "fs";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HOOK_BUDGETS
3
- } from "./chunk-Z5PL3K7C.js";
3
+ } from "./chunk-CJBVT33U.js";
4
4
 
5
5
  // src/codex-config.ts
6
6
  import { createHash } from "crypto";