@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.
- package/dist/bin/pushary-claude.js +227 -32
- package/dist/bin/pushary-clean.js +3 -3
- package/dist/bin/pushary-codex-hook.js +5 -5
- package/dist/bin/pushary-codex.js +2 -2
- package/dist/bin/pushary-daemon.js +22 -3
- package/dist/bin/pushary-doctor.js +3 -3
- package/dist/bin/pushary-gemini-hook.js +4 -4
- package/dist/bin/pushary-hook.js +5 -5
- package/dist/bin/pushary-notification-hook.js +2 -2
- package/dist/bin/pushary-permission-denied-hook.js +5 -5
- package/dist/bin/pushary-permission-hook.js +5 -5
- package/dist/bin/pushary-post-hook.js +2 -2
- package/dist/bin/pushary-prompt-hook.js +2 -2
- package/dist/bin/pushary-session-end-hook.js +2 -2
- package/dist/bin/pushary-session-start-hook.js +2 -2
- package/dist/bin/pushary-setup.js +4 -4
- package/dist/bin/pushary-stats.js +2 -2
- package/dist/bin/pushary-stop-hook.js +2 -2
- package/dist/bin/pushary-stopfailure-hook.js +2 -2
- package/dist/bin/pushary-upgrade.js +2 -2
- package/dist/{chunk-O5MFSRWV.js → chunk-BEK4SJNX.js} +27 -11
- package/dist/chunk-CJBVT33U.js +595 -0
- package/dist/{chunk-7EW3USQF.js → chunk-K74PMS6N.js} +1 -1
- package/dist/{chunk-R5AJNXZS.js → chunk-LRGSMQH2.js} +1 -1
- package/dist/{chunk-SVVEC4QV.js → chunk-ORKNGSJI.js} +1 -1
- package/dist/{chunk-OQMEGREX.js → chunk-ZIQKFF4S.js} +86 -23
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +5 -5
- package/package.json +4 -3
- package/dist/chunk-Z5PL3K7C.js +0 -322
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PRETOOLUSE_HANDLED_TOOLS
|
|
3
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
33
|
+
} from "./chunk-LRGSMQH2.js";
|
|
34
34
|
import {
|
|
35
|
+
buildDecisionEpisodeFeatures,
|
|
35
36
|
effectiveWaitSeconds,
|
|
36
37
|
hookWaitClamped,
|
|
37
38
|
hookWaitDeadline
|
|
38
|
-
} from "./chunk-
|
|
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
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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-
|
|
4
|
-
import "../chunk-
|
|
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-
|
|
18
|
+
} from "../chunk-BEK4SJNX.js";
|
|
19
19
|
import "../chunk-RN3NOEJF.js";
|
|
20
|
-
import "../chunk-
|
|
20
|
+
import "../chunk-LRGSMQH2.js";
|
|
21
21
|
import "../chunk-DWED7BS3.js";
|
|
22
|
-
import "../chunk-
|
|
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.
|
|
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:*",
|
package/dist/chunk-Z5PL3K7C.js
DELETED
|
@@ -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
|
-
};
|