@pushary/agent-hooks 0.50.0 → 0.52.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 +226 -31
- package/dist/bin/pushary-clean.js +3 -3
- package/dist/bin/pushary-codex-hook.js +3 -3
- 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 +3 -3
- 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-SCXPHMAS.js → chunk-5U3YC3BZ.js} +8 -4
- package/dist/{chunk-V42K3XQI.js → chunk-CANZPNGD.js} +1 -1
- package/dist/{chunk-WI3F6XNZ.js → chunk-F4DGHFIW.js} +241 -1
- package/dist/{chunk-FDRSMTJS.js → chunk-G3Y5HB4P.js} +1 -1
- package/dist/{chunk-M24KUFLW.js → chunk-JKSSDN2O.js} +88 -22
- package/dist/{chunk-PUT2N5VA.js → chunk-YTW7BZUC.js} +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +5 -5
- package/package.json +2 -2
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
handleSessionEnd
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-5U3YC3BZ.js";
|
|
5
5
|
import "../chunk-RN3NOEJF.js";
|
|
6
6
|
import "../chunk-DWED7BS3.js";
|
|
7
|
-
import "../chunk-
|
|
7
|
+
import "../chunk-F4DGHFIW.js";
|
|
8
8
|
import "../chunk-NKXSILEW.js";
|
|
9
9
|
|
|
10
10
|
// bin/pushary-session-end-hook.ts
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
handleSessionStart
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-5U3YC3BZ.js";
|
|
5
5
|
import "../chunk-RN3NOEJF.js";
|
|
6
6
|
import "../chunk-DWED7BS3.js";
|
|
7
|
-
import "../chunk-
|
|
7
|
+
import "../chunk-F4DGHFIW.js";
|
|
8
8
|
import "../chunk-NKXSILEW.js";
|
|
9
9
|
|
|
10
10
|
// bin/pushary-session-start-hook.ts
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
renderAgentInstructions,
|
|
14
14
|
renderProjectAgentInstructions,
|
|
15
15
|
writeInstructionBlock
|
|
16
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-G3Y5HB4P.js";
|
|
17
17
|
import {
|
|
18
18
|
execNpm,
|
|
19
19
|
npmErrorMessage,
|
|
@@ -25,15 +25,15 @@ import {
|
|
|
25
25
|
addClaudeMcpServer,
|
|
26
26
|
addPusharyHooks,
|
|
27
27
|
addPusharyToolPermissions
|
|
28
|
-
} from "../chunk-
|
|
28
|
+
} from "../chunk-YTW7BZUC.js";
|
|
29
29
|
import {
|
|
30
30
|
reportEvent
|
|
31
|
-
} from "../chunk-
|
|
31
|
+
} from "../chunk-5U3YC3BZ.js";
|
|
32
32
|
import "../chunk-RN3NOEJF.js";
|
|
33
33
|
import "../chunk-DWED7BS3.js";
|
|
34
34
|
import {
|
|
35
35
|
isValidApiKey
|
|
36
|
-
} from "../chunk-
|
|
36
|
+
} from "../chunk-F4DGHFIW.js";
|
|
37
37
|
import "../chunk-NKXSILEW.js";
|
|
38
38
|
|
|
39
39
|
// bin/pushary-setup.ts
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
handleStop
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-5U3YC3BZ.js";
|
|
5
5
|
import "../chunk-RN3NOEJF.js";
|
|
6
6
|
import "../chunk-DWED7BS3.js";
|
|
7
|
-
import "../chunk-
|
|
7
|
+
import "../chunk-F4DGHFIW.js";
|
|
8
8
|
import "../chunk-NKXSILEW.js";
|
|
9
9
|
|
|
10
10
|
// bin/pushary-stop-hook.ts
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
handleStopFailure
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-5U3YC3BZ.js";
|
|
5
5
|
import "../chunk-RN3NOEJF.js";
|
|
6
6
|
import "../chunk-DWED7BS3.js";
|
|
7
|
-
import "../chunk-
|
|
7
|
+
import "../chunk-F4DGHFIW.js";
|
|
8
8
|
import "../chunk-NKXSILEW.js";
|
|
9
9
|
|
|
10
10
|
// bin/pushary-stopfailure-hook.ts
|
|
@@ -8,8 +8,8 @@ import {
|
|
|
8
8
|
addClaudeMcpServer,
|
|
9
9
|
addPusharyHooks,
|
|
10
10
|
addPusharyToolPermissions
|
|
11
|
-
} from "../chunk-
|
|
12
|
-
import "../chunk-
|
|
11
|
+
} from "../chunk-YTW7BZUC.js";
|
|
12
|
+
import "../chunk-F4DGHFIW.js";
|
|
13
13
|
import {
|
|
14
14
|
getApiKey
|
|
15
15
|
} from "../chunk-NKXSILEW.js";
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
matchToolPattern,
|
|
17
17
|
redactSecrets,
|
|
18
18
|
redactSecretsDeep
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-F4DGHFIW.js";
|
|
20
20
|
import {
|
|
21
21
|
getApiKey,
|
|
22
22
|
getBaseUrl
|
|
@@ -168,17 +168,18 @@ var fetchModeState = async (apiKey, sessionId) => {
|
|
|
168
168
|
headers: { "Authorization": `Bearer ${apiKey}` },
|
|
169
169
|
signal: AbortSignal.timeout(3e3)
|
|
170
170
|
});
|
|
171
|
-
if (!response.ok) return { mode: null, kill: false, policyVersion: null, relayUrl: null };
|
|
171
|
+
if (!response.ok) return { mode: null, kill: false, policyVersion: null, relayUrl: null, trainingConsent: false };
|
|
172
172
|
const data = await response.json();
|
|
173
173
|
const mode = data.override?.mode;
|
|
174
174
|
return {
|
|
175
175
|
mode: isApprovalMode(mode) ? mode : null,
|
|
176
176
|
kill: data.kill === true,
|
|
177
177
|
policyVersion: toPolicyVersion(data.policyVersion),
|
|
178
|
-
relayUrl: typeof data.relayUrl === "string" && data.relayUrl.length > 0 ? data.relayUrl : null
|
|
178
|
+
relayUrl: typeof data.relayUrl === "string" && data.relayUrl.length > 0 ? data.relayUrl : null,
|
|
179
|
+
trainingConsent: data.trainingConsent === true
|
|
179
180
|
};
|
|
180
181
|
} catch {
|
|
181
|
-
return { mode: null, kill: false, policyVersion: null, relayUrl: null };
|
|
182
|
+
return { mode: null, kill: false, policyVersion: null, relayUrl: null, trainingConsent: false };
|
|
182
183
|
}
|
|
183
184
|
};
|
|
184
185
|
var fetchModeOverride = async (apiKey) => (await fetchModeState(apiKey)).mode;
|
|
@@ -807,6 +808,9 @@ var reportEvent = async (event, options = {}) => {
|
|
|
807
808
|
...event,
|
|
808
809
|
machineId: event.machineId ?? getMachineId(),
|
|
809
810
|
installMode: detectInstallMode(),
|
|
811
|
+
// Link-back for a phone-spawned session: forward the spawn id the daemon
|
|
812
|
+
// set in the environment so the server can map spawnId -> sessionId.
|
|
813
|
+
...process.env.PUSHARY_SPAWN_ID ? { spawnId: process.env.PUSHARY_SPAWN_ID } : {},
|
|
810
814
|
// Advertise that this client reads `pendingCommand` back off the
|
|
811
815
|
// response, so the server may drain the phone-queued instruction on
|
|
812
816
|
// activity events (tool_complete/tool_error/user_prompt), not only Stop.
|
|
@@ -296,6 +296,19 @@ var APPROVAL_FATIGUE_FLAG_PATTERNS = [
|
|
|
296
296
|
/\bterraform\s+(apply|destroy)\b/i,
|
|
297
297
|
/\bdocker\s+system\s+prune\b/i
|
|
298
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
|
+
]);
|
|
299
312
|
var HIGH_CONSEQUENCE_PATTERNS = [
|
|
300
313
|
/(^|[\\/])\.env(\.|$|[\\/])/i,
|
|
301
314
|
/secrets?/i,
|
|
@@ -304,6 +317,74 @@ var HIGH_CONSEQUENCE_PATTERNS = [
|
|
|
304
317
|
/\.github[\\/]workflows/i,
|
|
305
318
|
/package\.json/i
|
|
306
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
|
+
};
|
|
307
388
|
var SECRET_REDACTION_RULES = [
|
|
308
389
|
{ pattern: /-----BEGIN[A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z0-9 ]*PRIVATE KEY-----/g, replacement: "[redacted key]" },
|
|
309
390
|
{ pattern: /\bsk-[A-Za-z0-9_-]{16,}\b/g, replacement: "[redacted]" },
|
|
@@ -335,6 +416,164 @@ var classifyDecisionRisk = (toolName, toolTarget) => {
|
|
|
335
416
|
if (matchesAny(HIGH_CONSEQUENCE_PATTERNS)) return "elevated";
|
|
336
417
|
return "standard";
|
|
337
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
|
+
};
|
|
338
577
|
|
|
339
578
|
export {
|
|
340
579
|
isApprovalMode,
|
|
@@ -351,5 +590,6 @@ export {
|
|
|
351
590
|
DECISION_LINE_MAX,
|
|
352
591
|
redactSecrets,
|
|
353
592
|
redactSecretsDeep,
|
|
354
|
-
classifyDecisionRisk
|
|
593
|
+
classifyDecisionRisk,
|
|
594
|
+
buildDecisionEpisodeFeatures
|
|
355
595
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PRETOOLUSE_HANDLED_TOOLS
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-YTW7BZUC.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-5U3YC3BZ.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-CANZPNGD.js";
|
|
34
34
|
import {
|
|
35
|
+
buildDecisionEpisodeFeatures,
|
|
35
36
|
effectiveWaitSeconds,
|
|
36
37
|
hookWaitClamped,
|
|
37
38
|
hookWaitDeadline
|
|
38
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-F4DGHFIW.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) => {
|
|
@@ -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
|
}
|
|
@@ -364,7 +427,10 @@ var toPermissionRequestOutput = (hookOutput) => {
|
|
|
364
427
|
return void 0;
|
|
365
428
|
};
|
|
366
429
|
var handlePermissionRequest = async (input) => {
|
|
367
|
-
if (PRETOOLUSE_HANDLED_TOOLS.has(input.tool_name))
|
|
430
|
+
if (PRETOOLUSE_HANDLED_TOOLS.has(input.tool_name)) {
|
|
431
|
+
const reclaimDeferredExec = input.tool_name !== "AskUserQuestion" && shouldDeferToNativeMode(input.permission_mode, input.tool_name);
|
|
432
|
+
if (!reclaimDeferredExec) return void 0;
|
|
433
|
+
}
|
|
368
434
|
if (input.tool_name.startsWith("mcp__pushary__")) return void 0;
|
|
369
435
|
let apiKey;
|
|
370
436
|
try {
|
|
@@ -381,7 +447,7 @@ var handlePermissionRequest = async (input) => {
|
|
|
381
447
|
if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "deny") {
|
|
382
448
|
return permReqDeny(`Denied by policy for ${toolPolicy.tool}`);
|
|
383
449
|
}
|
|
384
|
-
return toPermissionRequestOutput(await dispatchModeHandler(apiKey, input, toolPolicy, getMachineId()));
|
|
450
|
+
return toPermissionRequestOutput(await dispatchModeHandler(apiKey, input, toolPolicy, getMachineId(), modeState.trainingConsent));
|
|
385
451
|
} catch {
|
|
386
452
|
return void 0;
|
|
387
453
|
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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-JKSSDN2O.js";
|
|
4
|
+
import "../chunk-YTW7BZUC.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-5U3YC3BZ.js";
|
|
19
19
|
import "../chunk-RN3NOEJF.js";
|
|
20
|
-
import "../chunk-
|
|
20
|
+
import "../chunk-CANZPNGD.js";
|
|
21
21
|
import "../chunk-DWED7BS3.js";
|
|
22
|
-
import "../chunk-
|
|
22
|
+
import "../chunk-F4DGHFIW.js";
|
|
23
23
|
import {
|
|
24
24
|
getApiKey,
|
|
25
25
|
getBaseUrl
|