@pushary/agent-hooks 0.85.0 → 0.85.2

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/data/cursor-plugin/CHANGELOG.md +21 -0
  3. package/data/cursor-plugin/scripts/pushary-gate.mjs +54 -222
  4. package/data/cursor-plugin/scripts/pushary-gate.test.mjs +11 -106
  5. package/data/vscode-plugin/CHANGELOG.md +26 -0
  6. package/data/vscode-plugin/scripts/pushary-gate.mjs +66 -74
  7. package/dist/bin/pushary-bell-hook.js +4 -4
  8. package/dist/bin/pushary-bell.js +6 -6
  9. package/dist/bin/pushary-claude.js +12 -11
  10. package/dist/bin/pushary-clean.js +6 -6
  11. package/dist/bin/pushary-codex-bridge.js +5 -5
  12. package/dist/bin/pushary-codex-hook.js +9 -9
  13. package/dist/bin/pushary-codex.js +3 -3
  14. package/dist/bin/pushary-connect.js +6 -6
  15. package/dist/bin/pushary-daemon.js +4 -4
  16. package/dist/bin/pushary-disconnect.js +5 -5
  17. package/dist/bin/pushary-doctor.js +11 -11
  18. package/dist/bin/pushary-gemini-bridge.js +5 -5
  19. package/dist/bin/pushary-gemini-hook.js +17 -12
  20. package/dist/bin/pushary-hook.js +8 -8
  21. package/dist/bin/pushary-login.js +4 -4
  22. package/dist/bin/pushary-logout.js +4 -4
  23. package/dist/bin/pushary-mode.js +3 -3
  24. package/dist/bin/pushary-notification-hook.js +3 -3
  25. package/dist/bin/pushary-permission-denied-hook.js +8 -8
  26. package/dist/bin/pushary-permission-hook.js +8 -8
  27. package/dist/bin/pushary-post-hook.js +3 -3
  28. package/dist/bin/pushary-prompt-hook.js +3 -3
  29. package/dist/bin/pushary-session-end-hook.js +3 -3
  30. package/dist/bin/pushary-session-start-hook.js +3 -3
  31. package/dist/bin/pushary-setup.js +14 -14
  32. package/dist/bin/pushary-stats.js +2 -2
  33. package/dist/bin/pushary-status.js +5 -5
  34. package/dist/bin/pushary-stop-hook.js +3 -3
  35. package/dist/bin/pushary-stopfailure-hook.js +3 -3
  36. package/dist/bin/pushary-upgrade.js +6 -6
  37. package/dist/bin/pushary-wait.js +3 -3
  38. package/dist/{chunk-ADISVXZL.js → chunk-4U2YRHNF.js} +1 -1
  39. package/dist/{chunk-WEAQQSHQ.js → chunk-4YMWLMAP.js} +1 -1
  40. package/dist/{chunk-437T77TY.js → chunk-CWV4CMB6.js} +1 -1
  41. package/dist/{chunk-EXMUM226.js → chunk-DPDUB4XF.js} +62 -156
  42. package/dist/{chunk-7PZPJSWI.js → chunk-GM74DUMS.js} +1 -1
  43. package/dist/{chunk-DOXSWT4Q.js → chunk-GY3I353G.js} +6 -6
  44. package/dist/chunk-HSGEEBBL.js +37 -0
  45. package/dist/{chunk-ERYKGH4J.js → chunk-JBLRZGTT.js} +1 -1
  46. package/dist/{chunk-4QPOMJUB.js → chunk-JT5NQQCE.js} +2 -2
  47. package/dist/{chunk-MQMPG5X4.js → chunk-K337LIBD.js} +260 -10
  48. package/dist/{chunk-HVZIHTOO.js → chunk-KJW6NKF7.js} +7 -7
  49. package/dist/{chunk-K7CSRGKZ.js → chunk-KZIGJLOX.js} +1 -1
  50. package/dist/{chunk-6LC4WSCL.js → chunk-LCA2EQTK.js} +1 -1
  51. package/dist/{chunk-F52SIIDG.js → chunk-LSB6PFLN.js} +1 -1
  52. package/dist/{chunk-YKW6JCWI.js → chunk-LXWISFF3.js} +1 -1
  53. package/dist/{chunk-FBGBJXVC.js → chunk-NTIUQB3E.js} +1 -1
  54. package/dist/{chunk-NJ7JT26G.js → chunk-PKUNB3SN.js} +1 -1
  55. package/dist/{chunk-AXRATIS6.js → chunk-Q4URN3E7.js} +1 -1
  56. package/dist/{chunk-GCKAWHFP.js → chunk-T54OT7IY.js} +1 -1
  57. package/dist/{chunk-64PXDATC.js → chunk-XJDL3MUE.js} +17 -17
  58. package/dist/{chunk-GFM3STWO.js → chunk-Y7E2MC3G.js} +20 -49
  59. package/dist/chunk-YHG74UFF.js +12 -0
  60. package/dist/src/index.d.ts +7 -29
  61. package/dist/src/index.js +10 -9
  62. package/package.json +4 -2
  63. package/dist/chunk-GXIFADGD.js +0 -66
  64. package/dist/chunk-PWTKKQQI.js +0 -27
@@ -65,6 +65,7 @@ var hookWaitDeadline = (startMs, policyWaitSeconds, agent, nowMs) => {
65
65
  };
66
66
  var hookWaitClamped = (startMs, policyWaitSeconds, agent, nowMs) => startMs + hookMaxWaitSeconds(agent) * 1e3 < nowMs + Math.max(policyWaitSeconds, 0) * 1e3;
67
67
  var effectiveWaitSeconds = (timeoutAction, policySeconds, agent) => timeoutAction === "wait" ? hookMaxWaitSeconds(agent) : policySeconds;
68
+ var HOOK_BATCH_MAX_BYTES = 4 * 1024 * 1024;
68
69
  var REPO_KEY_MAX_LENGTH = 200;
69
70
  var isRepoKeyMatch = (ruleRepoKey, currentRepoKey) => {
70
71
  if (!ruleRepoKey) return true;
@@ -635,6 +636,88 @@ var SECRET_REDACTION_RULES = [
635
636
  var HIGH_ENTROPY_RULE = { pattern: /[A-Za-z0-9+/]{40,}={0,2}/g, replacement: "[redacted]" };
636
637
  var redactSecrets = (text) => SECRET_REDACTION_RULES.reduce((acc, rule) => acc.replace(rule.pattern, rule.replacement), text);
637
638
  var redactSecretsDeep = (text) => redactSecrets(text).replace(HIGH_ENTROPY_RULE.pattern, HIGH_ENTROPY_RULE.replacement);
639
+ var AUTHORIZATION_CANONICAL_VERSION = "pushary.authz.v1";
640
+ var AUTHORIZATION_CANONICAL_FIXTURE = [
641
+ {
642
+ subject: { toolName: "refund.create" },
643
+ canonical: `${AUTHORIZATION_CANONICAL_VERSION}
644
+ action:13:refund.create
645
+ target:0:
646
+ actor:0:
647
+ environment:0:
648
+ externalId:0:`
649
+ },
650
+ {
651
+ subject: {
652
+ toolName: "refund.create",
653
+ toolTarget: "ord_991",
654
+ actor: "user:u_44",
655
+ environment: "production",
656
+ externalId: "cust_7",
657
+ parameters: { currency: "EUR", amount: 4800, expedited: true, rate: 1.5 }
658
+ },
659
+ canonical: `${AUTHORIZATION_CANONICAL_VERSION}
660
+ action:13:refund.create
661
+ target:7:ord_991
662
+ actor:9:user:u_44
663
+ environment:10:production
664
+ externalId:6:cust_7
665
+ p:13:amount=n:4800
666
+ p:14:currency=s:EUR
667
+ p:16:expedited=b:true
668
+ p:10:rate=n:1.5`
669
+ },
670
+ {
671
+ // Parameter order is not part of the subject. The same facts written in a
672
+ // different order are the same authorization.
673
+ subject: { toolName: "refund.create", parameters: { b: 2, a: 1 } },
674
+ canonical: `${AUTHORIZATION_CANONICAL_VERSION}
675
+ action:13:refund.create
676
+ target:0:
677
+ actor:0:
678
+ environment:0:
679
+ externalId:0:
680
+ p:5:a=n:1
681
+ p:5:b=n:2`
682
+ },
683
+ {
684
+ // An integral float is the same number as the integer.
685
+ subject: { toolName: "a", parameters: { n: 49 } },
686
+ canonical: `${AUTHORIZATION_CANONICAL_VERSION}
687
+ action:1:a
688
+ target:0:
689
+ actor:0:
690
+ environment:0:
691
+ externalId:0:
692
+ p:6:n=n:49`
693
+ },
694
+ {
695
+ // Delimiters inside a value cannot forge another subject.
696
+ subject: { toolName: "a", parameters: { "x\ny": "1" } },
697
+ canonical: `${AUTHORIZATION_CANONICAL_VERSION}
698
+ action:1:a
699
+ target:0:
700
+ actor:0:
701
+ environment:0:
702
+ externalId:0:
703
+ p:7:x
704
+ y=s:1`
705
+ },
706
+ {
707
+ // Every finite number, including the ones another language would render
708
+ // differently. One runtime computes both sides of the comparison.
709
+ subject: { toolName: "a", parameters: { small: 1e-7, big: 1e21 } },
710
+ canonical: `${AUTHORIZATION_CANONICAL_VERSION}
711
+ action:1:a
712
+ target:0:
713
+ actor:0:
714
+ environment:0:
715
+ externalId:0:
716
+ p:11:big=n:1e+21
717
+ p:12:small=n:1e-7`
718
+ }
719
+ ];
720
+ var AUTHORIZATION_PERMIT_TTL_SECONDS = 60 * 60;
638
721
  var classifyDecisionRisk = (toolName, toolTarget) => {
639
722
  const parts = [toolName, toolTarget ?? ""];
640
723
  const matchesAny = (patterns) => parts.some((part) => patterns.some((re) => re.test(part)));
@@ -833,9 +916,177 @@ var isScopeContract = (value) => {
833
916
  const strings = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
834
917
  return strings(c.allowedPaths) && strings(c.offLimitsPaths) && typeof c.doneWhen === "string";
835
918
  };
919
+ var autoApprove = (tool) => ({
920
+ tool,
921
+ timeoutSeconds: 0,
922
+ timeoutAction: "approve",
923
+ mode: "terminal_only",
924
+ pushFirstSeconds: 0
925
+ });
926
+ var resolveAutoResolveOrigin = (config, toolName, toolInput, cwd, repoKey, sessionId) => {
927
+ const args = toolInput ? policyArgForms(toolName, toolInput, cwd) : [];
928
+ const arg = args[0];
929
+ const match = selectGoverningRule(config.policies, toolName, args, repoKey, sessionId);
930
+ const governedBySpecificRule = match?.rank === "exact" || match?.rank === "prefix";
931
+ if (!governedBySpecificRule && toolName === "Bash" && typeof arg === "string" && isSafeReadOnlyCommand(arg)) {
932
+ return "safe_readonly";
933
+ }
934
+ return "policy_timeout";
935
+ };
936
+ var resolvePolicy = (config, toolName, modeOverride, toolInput, cwd, repoKey, sessionId) => {
937
+ const args = toolInput ? policyArgForms(toolName, toolInput, cwd) : [];
938
+ const arg = args[0];
939
+ const match = selectGoverningRule(config.policies, toolName, args, repoKey, sessionId);
940
+ let base = match?.policy ?? config.policies.find((p) => p.tool === "*") ?? {
941
+ tool: toolName,
942
+ timeoutSeconds: config.defaultTimeoutSeconds,
943
+ timeoutAction: config.defaultTimeoutAction,
944
+ mode: config.defaultMode ?? "push_first",
945
+ pushFirstSeconds: config.defaultPushFirstSeconds ?? 20
946
+ };
947
+ const governedBySpecificRule = match?.rank === "exact" || match?.rank === "prefix";
948
+ if (!governedBySpecificRule && toolName === "Bash" && typeof arg === "string" && isSafeReadOnlyCommand(arg)) {
949
+ base = autoApprove(base.tool);
950
+ }
951
+ const wouldAutoApprove = isAutoApprove(base);
952
+ if (!governedBySpecificRule && wouldAutoApprove && toolName === "Bash" && typeof arg === "string" && classifyDecisionRisk(toolName, arg) === "destructive") {
953
+ base = {
954
+ tool: base.tool,
955
+ timeoutSeconds: config.defaultTimeoutSeconds,
956
+ timeoutAction: "wait",
957
+ mode: "push_first",
958
+ pushFirstSeconds: config.defaultPushFirstSeconds ?? 20
959
+ };
960
+ }
961
+ const effectiveOverride = modeOverride ?? config.modeOverride;
962
+ if (effectiveOverride) {
963
+ return { ...base, mode: effectiveOverride };
964
+ }
965
+ return base;
966
+ };
967
+ var restraintRank = (policy) => {
968
+ if (isAutoDeny(policy)) return 4;
969
+ if (isAutoApprove(policy)) return 0;
970
+ if (policy.mode === "push_only") return 3;
971
+ return policy.mode === "terminal_only" || policy.mode === "notify_only" ? 1 : 2;
972
+ };
973
+ var strictestPolicy = (a, b) => restraintRank(b) > restraintRank(a) ? b : a;
974
+ var resolvePolicyAcross = (config, toolName, modeOverride, toolInputs, cwd, repoKey, sessionId) => {
975
+ if (toolInputs.length === 0) return resolvePolicy(config, toolName, modeOverride, void 0, cwd, repoKey, sessionId);
976
+ return toolInputs.map((input) => resolvePolicy(config, toolName, modeOverride, input, cwd, repoKey, sessionId)).reduce(strictestPolicy);
977
+ };
978
+ var isModeStateDegraded = (state) => state?.degraded === true;
979
+ var KILL_REASON = "Stopped by user, this agent was halted from Pushary";
980
+ var strictestScopeVerdict = (scope, paths) => {
981
+ if (paths.length === 0) return evaluateScope(scope, void 0);
982
+ let worst = { outcome: "in_scope" };
983
+ for (const path of paths) {
984
+ const verdict = evaluateScope(scope, path);
985
+ if (verdict.outcome === "off_limits") return verdict;
986
+ if (verdict.outcome === "out_of_scope") worst = verdict;
987
+ }
988
+ return worst;
989
+ };
990
+ var survivesDegraded = (config, toolName, toolInputs, cwd, repoKey, sessionId) => toolInputs.length === 1 && resolveAutoResolveOrigin(config, toolName, toolInputs[0], cwd, repoKey, sessionId) === "safe_readonly";
991
+ var resolveGate = (input) => {
992
+ const { modeState, config, toolName, toolInputs, cwd, repoKey, sessionId } = input;
993
+ if (modeState.kill) return { kind: "kill", reason: KILL_REASON };
994
+ const policy = resolvePolicyAcross(config, toolName, modeState.mode, toolInputs, cwd, repoKey, sessionId);
995
+ const verdict = verdictForPolicy(policy);
996
+ const autoApproves = verdict === "allow";
997
+ const autoDenies = verdict === "deny";
998
+ if (autoDenies) return { kind: "deny", policy, reason: `Denied by policy for ${policy.tool}` };
999
+ if (isModeStateDegraded(modeState)) {
1000
+ if (autoApproves) {
1001
+ return survivesDegraded(config, toolName, toolInputs, cwd, repoKey, sessionId) ? { kind: "allow", policy, source: "allowlist" } : { kind: "defer", policy };
1002
+ }
1003
+ return { kind: "gate", policy };
1004
+ }
1005
+ const scopeVerdict = modeState.scope ? strictestScopeVerdict(modeState.scope, input.scopePaths ?? []) : { outcome: "no_contract" };
1006
+ if (autoApproves) {
1007
+ if (!scopeRequiresHuman(scopeVerdict)) {
1008
+ return {
1009
+ kind: "allow",
1010
+ policy,
1011
+ source: toolInputs.length === 1 && resolveAutoResolveOrigin(config, toolName, toolInputs[0], cwd, repoKey, sessionId) === "safe_readonly" ? "allowlist" : "policy"
1012
+ };
1013
+ }
1014
+ }
1015
+ const scopeReason = scopeChangeReason(scopeVerdict);
1016
+ return {
1017
+ kind: "gate",
1018
+ policy,
1019
+ scopeReason,
1020
+ scopePath: scopeReason && "path" in scopeVerdict ? scopeVerdict.path : void 0
1021
+ };
1022
+ };
1023
+ var toPolicyVersion = (value) => typeof value === "string" || typeof value === "number" ? String(value) : null;
1024
+ var modeStateFromResponse = (data) => {
1025
+ const mode = data.override?.mode;
1026
+ return {
1027
+ mode: isApprovalMode(mode) ? mode : null,
1028
+ kill: data.kill === true,
1029
+ policyVersion: toPolicyVersion(data.policyVersion),
1030
+ relayUrl: typeof data.relayUrl === "string" && data.relayUrl.length > 0 ? data.relayUrl : null,
1031
+ trainingConsent: data.trainingConsent === true,
1032
+ // Validated rather than cast: a malformed contract must read as no contract,
1033
+ // never as one that silently matches nothing.
1034
+ scope: isScopeContract(data.scope) ? data.scope : null,
1035
+ degraded: false
1036
+ };
1037
+ };
1038
+ var claudePreToolUseOutput = (decision, reason, updatedInput) => ({
1039
+ hookSpecificOutput: {
1040
+ hookEventName: "PreToolUse",
1041
+ permissionDecision: decision,
1042
+ ...reason ? { permissionDecisionReason: reason } : {},
1043
+ ...updatedInput ? { updatedInput } : {}
1044
+ }
1045
+ });
1046
+ var claudePermissionRequestOutput = (decision, reason, updatedInput) => ({
1047
+ hookSpecificOutput: {
1048
+ hookEventName: "PermissionRequest",
1049
+ decision: decision === "allow" ? { behavior: "allow", ...updatedInput ? { updatedInput } : {} } : { behavior: "deny", ...reason ? { message: reason } : {} }
1050
+ }
1051
+ });
1052
+ var gateOutputFor = (source, event, decision, reason) => {
1053
+ let output;
1054
+ if (source === "claude") {
1055
+ if (event === "PreToolUse") {
1056
+ output = claudePreToolUseOutput(decision, reason);
1057
+ } else if (event === "PermissionRequest" && decision !== "ask") {
1058
+ output = claudePermissionRequestOutput(decision, reason);
1059
+ }
1060
+ } else if (source === "codex") {
1061
+ if (event === "PermissionRequest" && decision !== "ask") {
1062
+ output = {
1063
+ hookSpecificOutput: {
1064
+ hookEventName: "PermissionRequest",
1065
+ decision: decision === "allow" ? { behavior: "allow" } : { behavior: "deny", message: reason ?? "Denied from Pushary" }
1066
+ }
1067
+ };
1068
+ } else if (event === "PreToolUse" && decision === "deny") {
1069
+ output = {
1070
+ hookSpecificOutput: {
1071
+ hookEventName: "PreToolUse",
1072
+ permissionDecision: "deny",
1073
+ permissionDecisionReason: reason ?? "Denied from Pushary"
1074
+ }
1075
+ };
1076
+ }
1077
+ } else if (source === "gemini" && event === "BeforeTool" && decision !== "ask") {
1078
+ output = decision === "allow" ? { decision: "allow" } : { decision: "deny", reason: reason ?? "Denied from Pushary" };
1079
+ } else if (source === "cursor" && event === "beforeShellExecution") {
1080
+ output = decision === "allow" ? { permission: "allow" } : decision === "ask" ? { permission: "ask" } : {
1081
+ permission: "deny",
1082
+ user_message: "Command denied via Pushary.",
1083
+ agent_message: reason ?? "Denied from Pushary"
1084
+ };
1085
+ }
1086
+ return output;
1087
+ };
836
1088
 
837
1089
  export {
838
- isApprovalMode,
839
1090
  parseAgentQuestionAnswers,
840
1091
  normalizeInstallSource,
841
1092
  HOOK_BUDGETS,
@@ -844,21 +1095,20 @@ export {
844
1095
  effectiveWaitSeconds,
845
1096
  normalizeRepoRemote,
846
1097
  localRepoKey,
847
- policyArgForms,
848
1098
  isSafeReadOnlyCommand,
849
1099
  isAutoApprove,
850
- isAutoDeny,
851
- verdictForPolicy,
852
- selectGoverningRule,
853
1100
  isValidApiKey,
854
1101
  ACTION_BODY_MAX,
855
1102
  DECISION_LINE_MAX,
856
1103
  redactSecrets,
857
1104
  redactSecretsDeep,
858
- classifyDecisionRisk,
859
1105
  buildDecisionEpisodeFeatures,
860
- evaluateScope,
861
- scopeRequiresHuman,
862
- scopeChangeReason,
863
- isScopeContract
1106
+ resolveAutoResolveOrigin,
1107
+ resolvePolicy,
1108
+ KILL_REASON,
1109
+ resolveGate,
1110
+ modeStateFromResponse,
1111
+ claudePreToolUseOutput,
1112
+ claudePermissionRequestOutput,
1113
+ gateOutputFor
864
1114
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HOOK_BUDGETS
3
- } from "./chunk-MQMPG5X4.js";
3
+ } from "./chunk-K337LIBD.js";
4
4
 
5
5
  // src/codex-config.ts
6
6
  import { createHash } from "crypto";
@@ -14,12 +14,12 @@ var codexHooksJson = () => join(codexHome(), "hooks.json");
14
14
  var codexAgentsMd = () => join(codexHome(), "AGENTS.md");
15
15
  var codexSkillDir = () => join(codexHome(), "skills", "pushary");
16
16
  var CODEX_HOOK_EVENTS = [
17
- { event: "PermissionRequest", matcher: "Bash|apply_patch", timeout: HOOK_BUDGETS.codex.budgetSeconds, statusMessage: "Waiting for your phone" },
18
- { event: "PreToolUse", matcher: "Bash|apply_patch", timeout: HOOK_BUDGETS.codex.budgetSeconds, statusMessage: "Checking Pushary policy" },
19
- { event: "PostToolUse", matcher: "Bash|apply_patch", timeout: 10 },
20
- { event: "UserPromptSubmit", timeout: 10 },
21
- { event: "Stop", timeout: 10 },
22
- { event: "SessionStart", matcher: "startup|resume", timeout: 10 }
17
+ { event: "PermissionRequest", matcher: "Bash|apply_patch", delivery: "approval", timeout: HOOK_BUDGETS.codex.budgetSeconds, statusMessage: "Waiting for your phone" },
18
+ { event: "PreToolUse", matcher: "Bash|apply_patch", delivery: "approval", timeout: HOOK_BUDGETS.codex.budgetSeconds, statusMessage: "Checking Pushary policy" },
19
+ { event: "PostToolUse", matcher: "Bash|apply_patch", delivery: "telemetry", timeout: 10 },
20
+ { event: "UserPromptSubmit", delivery: "telemetry", timeout: 10 },
21
+ { event: "Stop", delivery: "telemetry", timeout: 10 },
22
+ { event: "SessionStart", matcher: "startup|resume", delivery: "telemetry", timeout: 10 }
23
23
  ];
24
24
  var CODEX_EVENT_KEY = {
25
25
  PermissionRequest: "permission_request",
@@ -9,7 +9,7 @@ import {
9
9
  } from "./chunk-6MTNS63X.js";
10
10
  import {
11
11
  resolveInstallSourceFor
12
- } from "./chunk-437T77TY.js";
12
+ } from "./chunk-CWV4CMB6.js";
13
13
  import {
14
14
  configFilePath
15
15
  } from "./chunk-2UMNXADU.js";
@@ -11,7 +11,7 @@ import {
11
11
  } from "./chunk-BSZYIAZL.js";
12
12
  import {
13
13
  reportedInstallSource
14
- } from "./chunk-437T77TY.js";
14
+ } from "./chunk-CWV4CMB6.js";
15
15
 
16
16
  // src/reach.ts
17
17
  var reachVerdict = (channels) => {
@@ -12,7 +12,7 @@ import {
12
12
  } from "./chunk-2UMNXADU.js";
13
13
  import {
14
14
  isValidApiKey
15
- } from "./chunk-MQMPG5X4.js";
15
+ } from "./chunk-K337LIBD.js";
16
16
 
17
17
  // src/session.ts
18
18
  var IDENTITY_PATH = "/api/v1/server/identity";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  pusharyDir
3
- } from "./chunk-HVZIHTOO.js";
3
+ } from "./chunk-KJW6NKF7.js";
4
4
  import {
5
5
  readJsonSafe
6
6
  } from "./chunk-6MTNS63X.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  describeKeyCheck,
3
3
  keyCheckFromResponse
4
- } from "./chunk-F52SIIDG.js";
4
+ } from "./chunk-LSB6PFLN.js";
5
5
  import {
6
6
  createIo
7
7
  } from "./chunk-5RUIFDTP.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  isSafeReadOnlyCommand
3
- } from "./chunk-MQMPG5X4.js";
3
+ } from "./chunk-K337LIBD.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
  parseVersion
3
- } from "./chunk-64PXDATC.js";
3
+ } from "./chunk-XJDL3MUE.js";
4
4
 
5
5
  // src/setup/agent-probe.ts
6
6
  import { execSync } from "child_process";
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-2UMNXADU.js";
4
4
  import {
5
5
  redactSecrets
6
- } from "./chunk-MQMPG5X4.js";
6
+ } from "./chunk-K337LIBD.js";
7
7
 
8
8
  // src/crypto.ts
9
9
  import nacl from "tweetnacl";
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  guardBinary
3
- } from "./chunk-PWTKKQQI.js";
3
+ } from "./chunk-HSGEEBBL.js";
4
4
  import {
5
5
  HOOK_BUDGETS
6
- } from "./chunk-MQMPG5X4.js";
6
+ } from "./chunk-K337LIBD.js";
7
7
 
8
8
  // src/claude-config.ts
9
9
  import { join } from "path";
@@ -40,23 +40,23 @@ var MIRRORED_TOOLS = [...PRETOOLUSE_GATED_TOOLS, "TodoWrite"].join("|");
40
40
  var ESTABLISHED = [1, 0, 0];
41
41
  var OBSERVED = [2, 0, 0];
42
42
  var CLAUDE_HOOK_EVENTS = [
43
- { event: "PreToolUse", binary: "pushary-hook", matcher: GATED_TOOLS, timeout: HOOK_BUDGETS.claude.budgetSeconds, since: ESTABLISHED },
44
- { event: "PostToolUse", binary: "pushary-post-hook", matcher: MIRRORED_TOOLS, timeout: 10, since: ESTABLISHED },
45
- { event: "Stop", binary: "pushary-stop-hook", timeout: 10, since: ESTABLISHED },
46
- { event: "UserPromptSubmit", binary: "pushary-prompt-hook", timeout: 10, since: ESTABLISHED },
47
- { event: "Notification", binary: "pushary-notification-hook", matcher: "permission_prompt|idle_prompt|agent_needs_input|agent_completed", timeout: 10, since: ESTABLISHED },
48
- { event: "SessionStart", binary: "pushary-session-start-hook", matcher: "startup|resume|clear", timeout: 10, since: ESTABLISHED },
49
- { event: "SessionEnd", binary: "pushary-session-end-hook", timeout: 10, since: ESTABLISHED },
50
- { event: "StopFailure", binary: "pushary-stopfailure-hook", timeout: 10, since: ESTABLISHED },
51
- { event: "PermissionRequest", binary: "pushary-permission-hook", timeout: HOOK_BUDGETS.claude.budgetSeconds, since: ESTABLISHED },
52
- { event: "PermissionDenied", binary: "pushary-permission-denied-hook", timeout: 60, since: ESTABLISHED },
43
+ { event: "PreToolUse", binary: "pushary-hook", matcher: GATED_TOOLS, timeout: HOOK_BUDGETS.claude.budgetSeconds, delivery: "approval", since: ESTABLISHED },
44
+ { event: "PostToolUse", binary: "pushary-post-hook", matcher: MIRRORED_TOOLS, timeout: 10, delivery: "telemetry", since: ESTABLISHED },
45
+ { event: "Stop", binary: "pushary-stop-hook", timeout: 10, delivery: "telemetry", since: ESTABLISHED },
46
+ { event: "UserPromptSubmit", binary: "pushary-prompt-hook", timeout: 10, delivery: "telemetry", since: ESTABLISHED },
47
+ { event: "Notification", binary: "pushary-notification-hook", matcher: "permission_prompt|idle_prompt|agent_needs_input|agent_completed", timeout: 10, delivery: "telemetry", since: ESTABLISHED },
48
+ { event: "SessionStart", binary: "pushary-session-start-hook", matcher: "startup|resume|clear", timeout: 10, delivery: "telemetry", since: ESTABLISHED },
49
+ { event: "SessionEnd", binary: "pushary-session-end-hook", timeout: 10, delivery: "telemetry", since: ESTABLISHED },
50
+ { event: "StopFailure", binary: "pushary-stopfailure-hook", timeout: 10, delivery: "telemetry", since: ESTABLISHED },
51
+ { event: "PermissionRequest", binary: "pushary-permission-hook", timeout: HOOK_BUDGETS.claude.budgetSeconds, delivery: "approval", since: ESTABLISHED },
52
+ { event: "PermissionDenied", binary: "pushary-permission-denied-hook", timeout: 60, delivery: "telemetry", since: ESTABLISHED },
53
53
  // Observability only. Each carries session shape the island cannot get any other
54
54
  // way, and none of them decides anything.
55
- { event: "SubagentStart", binary: "pushary-session-start-hook", timeout: 10, since: OBSERVED },
56
- { event: "SubagentStop", binary: "pushary-stop-hook", timeout: 10, since: OBSERVED },
57
- { event: "PreCompact", binary: "pushary-post-hook", matcher: "manual|auto", timeout: 10, since: OBSERVED },
58
- { event: "PostCompact", binary: "pushary-post-hook", matcher: "manual|auto", timeout: 10, since: OBSERVED },
59
- { event: "TeammateIdle", binary: "pushary-notification-hook", timeout: 10, since: OBSERVED }
55
+ { event: "SubagentStart", binary: "pushary-session-start-hook", timeout: 10, delivery: "telemetry", since: OBSERVED },
56
+ { event: "SubagentStop", binary: "pushary-stop-hook", timeout: 10, delivery: "telemetry", since: OBSERVED },
57
+ { event: "PreCompact", binary: "pushary-post-hook", matcher: "manual|auto", timeout: 10, delivery: "telemetry", since: OBSERVED },
58
+ { event: "PostCompact", binary: "pushary-post-hook", matcher: "manual|auto", timeout: 10, delivery: "telemetry", since: OBSERVED },
59
+ { event: "TeammateIdle", binary: "pushary-notification-hook", timeout: 10, delivery: "telemetry", since: OBSERVED }
60
60
  ];
61
61
  var supportedClaudeEvents = (version) => CLAUDE_HOOK_EVENTS.filter((hook) => version ? atLeast(version, hook.since) : hook.since === ESTABLISHED);
62
62
 
@@ -1,15 +1,14 @@
1
1
  import {
2
2
  PRETOOLUSE_HANDLED_TOOLS
3
- } from "./chunk-64PXDATC.js";
3
+ } from "./chunk-XJDL3MUE.js";
4
4
  import {
5
5
  isGatingMoment,
6
6
  recordKeylessMoment
7
- } from "./chunk-NJ7JT26G.js";
7
+ } from "./chunk-PKUNB3SN.js";
8
8
  import {
9
9
  denyReasonFrom,
10
- isDeferAnswer,
11
- resolveGate
12
- } from "./chunk-GXIFADGD.js";
10
+ isDeferAnswer
11
+ } from "./chunk-YHG74UFF.js";
13
12
  import {
14
13
  DEFAULT_SESSION,
15
14
  askUser,
@@ -29,7 +28,7 @@ import {
29
28
  sendNotification,
30
29
  throttlePass,
31
30
  waitForAnswer
32
- } from "./chunk-EXMUM226.js";
31
+ } from "./chunk-DPDUB4XF.js";
33
32
  import {
34
33
  getMachineId
35
34
  } from "./chunk-RN3NOEJF.js";
@@ -39,11 +38,14 @@ import {
39
38
  } from "./chunk-2UMNXADU.js";
40
39
  import {
41
40
  buildDecisionEpisodeFeatures,
41
+ claudePermissionRequestOutput,
42
+ claudePreToolUseOutput,
42
43
  effectiveWaitSeconds,
43
44
  hookWaitClamped,
44
45
  hookWaitDeadline,
45
- parseAgentQuestionAnswers
46
- } from "./chunk-MQMPG5X4.js";
46
+ parseAgentQuestionAnswers,
47
+ resolveGate
48
+ } from "./chunk-K337LIBD.js";
47
49
 
48
50
  // src/decision-episode.ts
49
51
  var EPISODE_PATH = "/api/agent/decision-episode";
@@ -73,33 +75,10 @@ import { tmpdir } from "os";
73
75
  import { existsSync, writeFileSync } from "fs";
74
76
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
75
77
  var START_MS = Date.now();
76
- var allow = () => ({
77
- hookSpecificOutput: {
78
- hookEventName: "PreToolUse",
79
- permissionDecision: "allow"
80
- }
81
- });
82
- var deny = (reason) => ({
83
- hookSpecificOutput: {
84
- hookEventName: "PreToolUse",
85
- permissionDecision: "deny",
86
- permissionDecisionReason: reason
87
- }
88
- });
89
- var allowWithInput = (updatedInput) => ({
90
- hookSpecificOutput: {
91
- hookEventName: "PreToolUse",
92
- permissionDecision: "allow",
93
- updatedInput
94
- }
95
- });
96
- var ask = (reason) => ({
97
- hookSpecificOutput: {
98
- hookEventName: "PreToolUse",
99
- permissionDecision: "ask",
100
- ...reason ? { permissionDecisionReason: reason } : {}
101
- }
102
- });
78
+ var allow = () => claudePreToolUseOutput("allow");
79
+ var deny = (reason) => claudePreToolUseOutput("deny", reason);
80
+ var allowWithInput = (updatedInput) => claudePreToolUseOutput("allow", void 0, updatedInput);
81
+ var ask = (reason) => claudePreToolUseOutput("ask", reason);
103
82
  var pollForAnswer = async (apiKey, correlationId, deadlineMs, pollInterval = 2e3) => {
104
83
  while (Date.now() < deadlineMs) {
105
84
  const remaining = Math.min(Math.max(deadlineMs - Date.now(), 1e3), 3e4);
@@ -513,14 +492,16 @@ var handlePreToolUse = async (input) => {
513
492
  if (input.tool_name === "AskUserQuestion") {
514
493
  return handleAskUserQuestion(apiKey, input);
515
494
  }
495
+ if (verdict.kind === "deny") return deny(verdict.reason);
516
496
  if (shouldDeferToNativeMode(input.permission_mode, input.tool_name)) {
517
497
  return void 0;
518
498
  }
519
499
  switch (verdict.kind) {
520
500
  case "allow":
521
501
  return allow();
522
- case "deny":
523
- return deny(verdict.reason);
502
+ // No `deny` case: it is handled above the defer, and the compiler enforces
503
+ // that it stays there. Restoring one here would type-error as unreachable.
504
+ //
524
505
  // State we could not establish, with an auto-approve on the table. No
525
506
  // opinion, so Claude Code's own flow runs as if the hook were absent.
526
507
  case "defer":
@@ -532,18 +513,8 @@ var handlePreToolUse = async (input) => {
532
513
  return void 0;
533
514
  }
534
515
  };
535
- var permReqAllow = (updatedInput) => ({
536
- hookSpecificOutput: {
537
- hookEventName: "PermissionRequest",
538
- decision: { behavior: "allow", ...updatedInput ? { updatedInput } : {} }
539
- }
540
- });
541
- var permReqDeny = (message) => ({
542
- hookSpecificOutput: {
543
- hookEventName: "PermissionRequest",
544
- decision: { behavior: "deny", ...message ? { message } : {} }
545
- }
546
- });
516
+ var permReqAllow = (updatedInput) => claudePermissionRequestOutput("allow", void 0, updatedInput);
517
+ var permReqDeny = (message) => claudePermissionRequestOutput("deny", message);
547
518
  var toPermissionRequestOutput = (hookOutput) => {
548
519
  if (!hookOutput) return void 0;
549
520
  const d = hookOutput.hookSpecificOutput;
@@ -0,0 +1,12 @@
1
+ // src/answer.ts
2
+ var denyReasonFrom = (value, note) => {
3
+ const trimmed = note?.trim();
4
+ if (trimmed) return `Denied from your phone: ${trimmed}`;
5
+ return value && value !== "no" && value !== "yes" ? `Denied from your phone: ${value}` : "Denied via push notification";
6
+ };
7
+ var isDeferAnswer = (value) => value === "defer";
8
+
9
+ export {
10
+ denyReasonFrom,
11
+ isDeferAnswer
12
+ };
@@ -1,5 +1,6 @@
1
- import { PolicyConfig, ApprovalMode, ToolPolicy, ScopeContract, AgentQuestion } from '@pushary/contracts';
2
- export { ApprovalMode, PolicyConfig, ToolPolicy } from '@pushary/contracts';
1
+ import { ClaudePreToolUseOutput, PolicyConfig, ApprovalMode, ModeState, AutoResolveOrigin, AgentQuestion } from '@pushary/contracts';
2
+ export { ApprovalMode, ModeState, PolicyConfig, ToolPolicy, resolvePolicy } from '@pushary/contracts';
3
+ import { ReceiptMeta } from '@pushary/hook-mapping';
3
4
 
4
5
  interface ToolInput {
5
6
  tool_name: string;
@@ -10,35 +11,11 @@ interface ToolInput {
10
11
  tool_use_id?: string;
11
12
  permission_mode?: string;
12
13
  }
13
- interface HookOutput {
14
- hookSpecificOutput: {
15
- hookEventName: 'PreToolUse';
16
- permissionDecision: 'allow' | 'deny' | 'ask';
17
- permissionDecisionReason?: string;
18
- updatedInput?: Record<string, unknown>;
19
- };
20
- }
14
+ type HookOutput = ClaudePreToolUseOutput;
21
15
  declare const handlePreToolUse: (input: ToolInput) => Promise<HookOutput | undefined>;
22
16
 
23
- type ReceiptKind = 'edit' | 'write' | 'bash' | 'commit';
24
- interface ReceiptMeta {
25
- kind: ReceiptKind;
26
- target: string;
27
- ok: boolean;
28
- }
29
-
30
17
  declare const getPolicy: (apiKey: string, expectedVersion?: string | null, agent?: string, repoAware?: boolean, sessionId?: string) => Promise<PolicyConfig>;
31
- type AutoResolveOrigin = 'safe_readonly' | 'policy_timeout';
32
- declare const resolvePolicy: (config: PolicyConfig, toolName: string, modeOverride?: ApprovalMode | null, toolInput?: Record<string, unknown>, cwd?: string, repoKey?: string, sessionId?: string) => ToolPolicy;
33
- interface ModeState {
34
- readonly mode: ApprovalMode | null;
35
- readonly kill: boolean;
36
- readonly policyVersion: string | null;
37
- readonly relayUrl: string | null;
38
- readonly trainingConsent?: boolean;
39
- readonly scope?: ScopeContract | null;
40
- readonly degraded?: boolean;
41
- }
18
+
42
19
  declare const fetchModeState: (apiKey: string, sessionId?: string, reuseWithinMs?: number) => Promise<ModeState>;
43
20
  declare const fetchModeOverride: (apiKey: string) => Promise<ApprovalMode | null>;
44
21
 
@@ -142,6 +119,7 @@ interface AskUserParams {
142
119
  actionBody?: string;
143
120
  scopePath?: string;
144
121
  toolUseId?: string;
122
+ requestId?: string;
145
123
  }
146
124
  interface AskUserResponse {
147
125
  correlationId: string;
@@ -166,4 +144,4 @@ declare const cancelQuestion: (apiKey: string, correlationId: string) => Promise
166
144
  declare const getApiKey: () => string;
167
145
  declare const getBaseUrl: () => string;
168
146
 
169
- export { type ModeState, askUser, cancelQuestion, fetchModeOverride, fetchModeState, getApiKey, getBaseUrl, getPolicy, handleNotification, handlePostToolUse, handlePreToolUse, handleStop, reportEvent, resolvePolicy, waitForAnswer };
147
+ export { askUser, cancelQuestion, fetchModeOverride, fetchModeState, getApiKey, getBaseUrl, getPolicy, handleNotification, handlePostToolUse, handlePreToolUse, handleStop, reportEvent, waitForAnswer };