@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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  PRETOOLUSE_HANDLED_TOOLS
3
- } from "./chunk-7EW3USQF.js";
3
+ } from "./chunk-K74PMS6N.js";
4
4
  import {
5
5
  denyReasonFrom,
6
6
  isDeferAnswer
@@ -23,23 +23,47 @@ import {
23
23
  sendNotification,
24
24
  throttlePass,
25
25
  waitForAnswer
26
- } from "./chunk-O5MFSRWV.js";
26
+ } from "./chunk-BEK4SJNX.js";
27
27
  import {
28
28
  getMachineId
29
29
  } from "./chunk-RN3NOEJF.js";
30
30
  import {
31
31
  isGatingMoment,
32
32
  recordKeylessMoment
33
- } from "./chunk-R5AJNXZS.js";
33
+ } from "./chunk-LRGSMQH2.js";
34
34
  import {
35
+ buildDecisionEpisodeFeatures,
35
36
  effectiveWaitSeconds,
36
37
  hookWaitClamped,
37
38
  hookWaitDeadline
38
- } from "./chunk-Z5PL3K7C.js";
39
+ } from "./chunk-CJBVT33U.js";
39
40
  import {
40
- getApiKey
41
+ getApiKey,
42
+ getBaseUrl
41
43
  } from "./chunk-NKXSILEW.js";
42
44
 
45
+ // src/decision-episode.ts
46
+ var EPISODE_PATH = "/api/agent/decision-episode";
47
+ var EMIT_TIMEOUT_MS = 3e3;
48
+ var buildEpisodeBody = (input) => ({
49
+ sessionId: input.sessionId,
50
+ machineId: input.machineId,
51
+ agentType: input.agentType,
52
+ features: buildDecisionEpisodeFeatures(input)
53
+ });
54
+ var reportDecisionEpisode = async (apiKey, input) => {
55
+ try {
56
+ const baseUrl = getBaseUrl();
57
+ await fetch(`${baseUrl}${EPISODE_PATH}`, {
58
+ method: "POST",
59
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
60
+ body: JSON.stringify(buildEpisodeBody(input)),
61
+ signal: AbortSignal.timeout(EMIT_TIMEOUT_MS)
62
+ });
63
+ } catch {
64
+ }
65
+ };
66
+
43
67
  // src/hook.ts
44
68
  import { basename, join } from "path";
45
69
  import { tmpdir } from "os";
@@ -90,7 +114,7 @@ var pollForAnswer = async (apiKey, correlationId, deadlineMs, pollInterval = 2e3
90
114
  }
91
115
  return { answered: false };
92
116
  };
93
- var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, timeoutAction, sessionId, machineId, toolName, toolTarget, decision) => {
117
+ var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, timeoutAction, sessionId, machineId, toolName, toolTarget, decision, signals) => {
94
118
  let result;
95
119
  try {
96
120
  result = await askUser(apiKey, {
@@ -136,6 +160,7 @@ var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, ti
136
160
  const answer = await pollForAnswer(apiKey, result.correlationId, deadline);
137
161
  if (answer.answered) {
138
162
  if (isDeferAnswer(answer.value)) return ask("Handling on your machine");
163
+ signals.humanDecision = true;
139
164
  return answer.value === "yes" ? allow() : deny(denyReasonFrom(answer.value));
140
165
  }
141
166
  if (timeoutAction === "wait" || clampCut) {
@@ -154,7 +179,7 @@ var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, ti
154
179
  var handleTerminalOnly = () => {
155
180
  return ask();
156
181
  };
157
- var handlePushFirst = async (apiKey, description, projectName, pushFirstSeconds, sessionId, machineId, toolName, toolTarget, decision) => {
182
+ var handlePushFirst = async (apiKey, description, projectName, pushFirstSeconds, sessionId, machineId, toolName, toolTarget, decision, signals) => {
158
183
  let result;
159
184
  try {
160
185
  result = await askUser(apiKey, {
@@ -183,6 +208,7 @@ var handlePushFirst = async (apiKey, description, projectName, pushFirstSeconds,
183
208
  const answer = await pollForAnswer(apiKey, result.correlationId, deadline, 1500);
184
209
  if (answer.answered) {
185
210
  if (isDeferAnswer(answer.value)) return ask("Handling on your machine");
211
+ signals.humanDecision = true;
186
212
  return answer.value === "yes" ? allow() : deny(denyReasonFrom(answer.value));
187
213
  }
188
214
  savePendingQuestion(sessionId || DEFAULT_SESSION, result.correlationId);
@@ -201,7 +227,36 @@ var handleNotifyOnly = async (apiKey, description, projectName, sessionId, machi
201
227
  }
202
228
  return ask();
203
229
  };
204
- var dispatchModeHandler = async (apiKey, input, toolPolicy, machineId) => {
230
+ var CLAUDE_AGENT_TYPE = "claude_code";
231
+ var emitDecisionEpisode = (apiKey, input, toolPolicy, machineId, toolTarget, decision, result) => {
232
+ const verdict = result?.hookSpecificOutput?.permissionDecision;
233
+ const outcome = verdict === "allow" ? "approve" : verdict === "deny" ? "deny" : null;
234
+ if (!outcome) return;
235
+ const ti = input.tool_input ?? {};
236
+ const commandOrPath = typeof ti.command === "string" ? ti.command : typeof ti.file_path === "string" ? ti.file_path : typeof ti.path === "string" ? ti.path : void 0;
237
+ void reportDecisionEpisode(apiKey, {
238
+ toolName: input.tool_name,
239
+ commandOrPath,
240
+ intent: decision.intent,
241
+ toolTarget,
242
+ agentType: CLAUDE_AGENT_TYPE,
243
+ matchedPattern: toolPolicy.tool,
244
+ policyMode: toolPolicy.mode,
245
+ timeoutSeconds: toolPolicy.timeoutSeconds,
246
+ timeoutAction: toolPolicy.timeoutAction,
247
+ // Reaching a handler means a human gate was requested (auto-allow/deny
248
+ // returned earlier in the caller before dispatch).
249
+ requestedGate: "ask",
250
+ decision: outcome,
251
+ // Truthfully human: the caller only emits when signals.humanDecision is set
252
+ // (a real phone answer). Bronze label until reason + execution-outcome land.
253
+ origin: "human",
254
+ latencyMs: Date.now() - START_MS,
255
+ sessionId: input.session_id,
256
+ machineId
257
+ });
258
+ };
259
+ var dispatchModeHandler = async (apiKey, input, toolPolicy, machineId, trainingConsent = false) => {
205
260
  const description = describeToolCall(input.tool_name, input.tool_input, "hook");
206
261
  const projectName = basename(input.cwd ?? process.cwd());
207
262
  const sessionId = input.session_id;
@@ -213,18 +268,26 @@ var dispatchModeHandler = async (apiKey, input, toolPolicy, machineId) => {
213
268
  blocker: deriveBlocker(toolPolicy.mode),
214
269
  actionBody: deriveActionBody(input.tool_name, input.tool_input)
215
270
  };
216
- switch (toolPolicy.mode) {
217
- case "push_only":
218
- return handlePushOnly(apiKey, description, projectName, toolPolicy.timeoutSeconds, toolPolicy.timeoutAction, sessionId, machineId, input.tool_name, toolTarget, decision);
219
- case "terminal_only":
220
- return handleTerminalOnly();
221
- case "push_first":
222
- return handlePushFirst(apiKey, description, projectName, toolPolicy.pushFirstSeconds, sessionId, machineId, input.tool_name, toolTarget, decision);
223
- case "notify_only":
224
- return handleNotifyOnly(apiKey, description, projectName, sessionId, machineId);
225
- default:
226
- return handlePushFirst(apiKey, description, projectName, toolPolicy.pushFirstSeconds, sessionId, machineId, input.tool_name, toolTarget, decision);
271
+ const signals = {};
272
+ const runHandler = () => {
273
+ switch (toolPolicy.mode) {
274
+ case "push_only":
275
+ return handlePushOnly(apiKey, description, projectName, toolPolicy.timeoutSeconds, toolPolicy.timeoutAction, sessionId, machineId, input.tool_name, toolTarget, decision, signals);
276
+ case "terminal_only":
277
+ return Promise.resolve(handleTerminalOnly());
278
+ case "push_first":
279
+ return handlePushFirst(apiKey, description, projectName, toolPolicy.pushFirstSeconds, sessionId, machineId, input.tool_name, toolTarget, decision, signals);
280
+ case "notify_only":
281
+ return handleNotifyOnly(apiKey, description, projectName, sessionId, machineId);
282
+ default:
283
+ return handlePushFirst(apiKey, description, projectName, toolPolicy.pushFirstSeconds, sessionId, machineId, input.tool_name, toolTarget, decision, signals);
284
+ }
285
+ };
286
+ const result = await runHandler();
287
+ if (trainingConsent && signals.humanDecision) {
288
+ emitDecisionEpisode(apiKey, input, toolPolicy, machineId, toolTarget, decision, result);
227
289
  }
290
+ return result;
228
291
  };
229
292
  var KEYLESS_NOTICE = "[pushary] Not connected (no API key), so your agent runs untouched. Approval moments are counted locally; see them with: npx @pushary/agent-hooks@latest stats. Connect: https://pushary.com/sign-up?utm_source=cli&utm_medium=keyless-hook";
230
293
  var keylessNoticeOnce = (sessionId) => {
@@ -322,7 +385,7 @@ var handlePreToolUse = async (input) => {
322
385
  }
323
386
  try {
324
387
  const modeState = await fetchModeState(apiKey, input.session_id);
325
- const policy = await getPolicy(apiKey, modeState.policyVersion);
388
+ const policy = await getPolicy(apiKey, modeState.policyVersion, "claude_code");
326
389
  if (modeState.kill) {
327
390
  return deny("Stopped by user \u2014 this agent was halted from Pushary");
328
391
  }
@@ -339,7 +402,7 @@ var handlePreToolUse = async (input) => {
339
402
  if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "deny") {
340
403
  return deny(`Denied by policy for ${toolPolicy.tool}`);
341
404
  }
342
- return dispatchModeHandler(apiKey, input, toolPolicy, getMachineId());
405
+ return dispatchModeHandler(apiKey, input, toolPolicy, getMachineId(), modeState.trainingConsent);
343
406
  } catch {
344
407
  return void 0;
345
408
  }
@@ -374,14 +437,14 @@ var handlePermissionRequest = async (input) => {
374
437
  }
375
438
  try {
376
439
  const modeState = await fetchModeState(apiKey, input.session_id);
377
- const policy = await getPolicy(apiKey, modeState.policyVersion);
440
+ const policy = await getPolicy(apiKey, modeState.policyVersion, "claude_code");
378
441
  if (modeState.kill) return permReqDeny("Stopped by user \u2014 this agent was halted from Pushary");
379
442
  const toolPolicy = resolvePolicy(policy, input.tool_name, modeState.mode, input.tool_input);
380
443
  if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") return permReqAllow();
381
444
  if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "deny") {
382
445
  return permReqDeny(`Denied by policy for ${toolPolicy.tool}`);
383
446
  }
384
- return toPermissionRequestOutput(await dispatchModeHandler(apiKey, input, toolPolicy, getMachineId()));
447
+ return toPermissionRequestOutput(await dispatchModeHandler(apiKey, input, toolPolicy, getMachineId(), modeState.trainingConsent));
385
448
  } catch {
386
449
  return void 0;
387
450
  }
@@ -26,7 +26,7 @@ interface ReceiptMeta {
26
26
  ok: boolean;
27
27
  }
28
28
 
29
- declare const getPolicy: (apiKey: string, expectedVersion?: string | null) => Promise<PolicyConfig>;
29
+ declare const getPolicy: (apiKey: string, expectedVersion?: string | null, agent?: string) => Promise<PolicyConfig>;
30
30
  type AutoResolveOrigin = 'safe_readonly' | 'policy_timeout';
31
31
  declare const resolvePolicy: (config: PolicyConfig, toolName: string, modeOverride?: ApprovalMode | null, toolInput?: Record<string, unknown>) => ToolPolicy;
32
32
  interface ModeState {
@@ -34,6 +34,7 @@ interface ModeState {
34
34
  readonly kill: boolean;
35
35
  readonly policyVersion: string | null;
36
36
  readonly relayUrl: string | null;
37
+ readonly trainingConsent?: boolean;
37
38
  }
38
39
  declare const fetchModeState: (apiKey: string, sessionId?: string) => Promise<ModeState>;
39
40
  declare const fetchModeOverride: (apiKey: string) => Promise<ApprovalMode | null>;
package/dist/src/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  handlePreToolUse
3
- } from "../chunk-OQMEGREX.js";
4
- import "../chunk-7EW3USQF.js";
3
+ } from "../chunk-ZIQKFF4S.js";
4
+ import "../chunk-K74PMS6N.js";
5
5
  import "../chunk-KQYIHZ5E.js";
6
6
  import {
7
7
  askUser,
@@ -15,11 +15,11 @@ import {
15
15
  reportEvent,
16
16
  resolvePolicy,
17
17
  waitForAnswer
18
- } from "../chunk-O5MFSRWV.js";
18
+ } from "../chunk-BEK4SJNX.js";
19
19
  import "../chunk-RN3NOEJF.js";
20
- import "../chunk-R5AJNXZS.js";
20
+ import "../chunk-LRGSMQH2.js";
21
21
  import "../chunk-DWED7BS3.js";
22
- import "../chunk-Z5PL3K7C.js";
22
+ import "../chunk-CJBVT33U.js";
23
23
  import {
24
24
  getApiKey,
25
25
  getBaseUrl
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.49.1",
3
+ "version": "0.51.0",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "keywords": [
6
6
  "pushary",
@@ -73,12 +73,13 @@
73
73
  "scripts": {
74
74
  "build": "node scripts/bundle-plugin.mjs && tsup",
75
75
  "dev": "tsup --watch",
76
- "test": "bun test src/api.test.ts && bun test src/claude-config.test.ts && bun test src/config.test.ts && bun test src/mcp-http.test.ts && bun test src/retry.test.ts && bun test src/usage.test.ts && bun test src/validate.test.ts && bun test src/policy.test.ts && bun test src/npm.test.ts && bun test src/identity.test.ts && bun test src/pending.test.ts && bun test src/events.test.ts && bun test src/describe.test.ts && bun test src/reapply.test.ts && bun test src/suggestions.test.ts && bun test src/safe-commands.test.ts && bun test src/hook.test.ts && bun test src/codex-adapter.test.ts && bun test src/codex-config.test.ts && bun test src/gemini-adapter.test.ts && bun test src/gemini-config.test.ts && bun test src/instruction-file.test.ts && bun test src/pairing.test.ts && bun test src/wait-ladder.test.ts && bun test src/ledger.test.ts && bun test src/wrapper/wrapper.test.ts && bun test src/wrapper/messageQueue.test.ts && bun test src/wrapper/approver.test.ts && bun test src/wrapper/remoteLoop.test.ts && bun test src/wrapper/spawnClaude.test.ts && bun test src/wrapper/wsProtocol.test.ts && bun test src/wrapper/relayClient.test.ts && bun test src/wrapper/modeLoop.test.ts && bun test src/wrapper/stdinHandoff.test.ts && bun test src/wrapper/localLeg.test.ts"
76
+ "test": "bun test src/api.test.ts && bun test src/claude-config.test.ts && bun test src/config.test.ts && bun test src/mcp-http.test.ts && bun test src/retry.test.ts && bun test src/usage.test.ts && bun test src/validate.test.ts && bun test src/policy.test.ts && bun test src/npm.test.ts && bun test src/identity.test.ts && bun test src/pending.test.ts && bun test src/events.test.ts && bun test src/describe.test.ts && bun test src/reapply.test.ts && bun test src/suggestions.test.ts && bun test src/safe-commands.test.ts && bun test src/hook.test.ts && bun test src/codex-adapter.test.ts && bun test src/codex-config.test.ts && bun test src/gemini-adapter.test.ts && bun test src/gemini-config.test.ts && bun test src/instruction-file.test.ts && bun test src/pairing.test.ts && bun test src/wait-ladder.test.ts && bun test src/ledger.test.ts && bun test src/wrapper/wrapper.test.ts && bun test src/wrapper/messageQueue.test.ts && bun test src/wrapper/approver.test.ts && bun test src/wrapper/remoteLoop.test.ts && bun test src/wrapper/spawnClaude.test.ts && bun test src/wrapper/wsProtocol.test.ts && bun test src/wrapper/relayClient.test.ts && bun test src/wrapper/modeLoop.test.ts && bun test src/wrapper/stdinHandoff.test.ts && bun test src/wrapper/localLeg.test.ts && bun test src/crypto.test.ts && bun test src/crypto-vector.test.ts && bun test src/transcript-capture.test.ts && bun test src/transcript-sync.test.ts && bun test src/spawn-daemon.test.ts && bun test src/decision-episode.test.ts"
77
77
  },
78
78
  "dependencies": {
79
79
  "@inquirer/prompts": "^8.4.2",
80
80
  "qrcode-terminal": "^0.12.0",
81
- "smol-toml": "^1.6.1"
81
+ "smol-toml": "^1.6.1",
82
+ "tweetnacl": "^1.0.3"
82
83
  },
83
84
  "devDependencies": {
84
85
  "@pushary/contracts": "workspace:*",
@@ -1,322 +0,0 @@
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 SECRET_REDACTION_RULES = [
283
- { pattern: /-----BEGIN[A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z0-9 ]*PRIVATE KEY-----/g, replacement: "[redacted key]" },
284
- { pattern: /\bsk-[A-Za-z0-9_-]{16,}\b/g, replacement: "[redacted]" },
285
- { pattern: /\b[spr]k_(?:live|test)_[A-Za-z0-9]{8,}\b/g, replacement: "[redacted]" },
286
- { pattern: /\bwhsec_[A-Za-z0-9]{16,}\b/g, replacement: "[redacted]" },
287
- { pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b/g, replacement: "[redacted]" },
288
- { pattern: /\bgithub_pat_[A-Za-z0-9_]{22,}\b/g, replacement: "[redacted]" },
289
- { pattern: /\bglpat-[A-Za-z0-9_-]{20,}\b/g, replacement: "[redacted]" },
290
- { pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, replacement: "[redacted]" },
291
- { pattern: /\bAIza[A-Za-z0-9_-]{35}\b/g, replacement: "[redacted]" },
292
- { pattern: /\bAKIA[0-9A-Z]{16}\b/g, replacement: "[redacted]" },
293
- { pattern: /\bnpm_[A-Za-z0-9]{36}\b/g, replacement: "[redacted]" },
294
- { pattern: /\bxai-[A-Za-z0-9]{16,}\b/g, replacement: "[redacted]" },
295
- { pattern: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, replacement: "[redacted]" },
296
- { pattern: /\bbearer\s+[A-Za-z0-9._~+/=-]+/gi, replacement: "bearer [redacted]" },
297
- { pattern: /\bauthorization:\s*\S+/gi, replacement: "authorization: [redacted]" },
298
- {
299
- pattern: /((?:secret|token|password|passwd|api[_-]?key|access[_-]?key|client[_-]?secret|private[_-]?key)\s*[=:]\s*)("[^"]*"|'[^']*'|\S+)/gi,
300
- replacement: "$1[redacted]"
301
- }
302
- ];
303
- var HIGH_ENTROPY_RULE = { pattern: /[A-Za-z0-9+/]{40,}={0,2}/g, replacement: "[redacted]" };
304
- var redactSecrets = (text) => SECRET_REDACTION_RULES.reduce((acc, rule) => acc.replace(rule.pattern, rule.replacement), text);
305
- var redactSecretsDeep = (text) => redactSecrets(text).replace(HIGH_ENTROPY_RULE.pattern, HIGH_ENTROPY_RULE.replacement);
306
-
307
- export {
308
- isApprovalMode,
309
- HOOK_BUDGETS,
310
- hookWaitDeadline,
311
- hookWaitClamped,
312
- effectiveWaitSeconds,
313
- matchRankWeight,
314
- matchToolPattern,
315
- extractPolicyArg,
316
- isSafeReadOnlyCommand,
317
- isValidApiKey,
318
- ACTION_BODY_MAX,
319
- DECISION_LINE_MAX,
320
- redactSecrets,
321
- redactSecretsDeep
322
- };