@pushary/agent-hooks 0.84.1 → 0.85.1
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/CHANGELOG.md +64 -0
- package/README.md +24 -0
- package/data/cursor-plugin/CHANGELOG.md +21 -0
- package/data/cursor-plugin/scripts/pushary-gate.mjs +54 -222
- package/data/cursor-plugin/scripts/pushary-gate.test.mjs +11 -106
- package/data/vscode-plugin/CHANGELOG.md +26 -0
- package/data/vscode-plugin/scripts/pushary-gate.mjs +66 -74
- package/dist/bin/pushary-bell-hook.js +9 -7
- package/dist/bin/pushary-bell.js +12 -11
- package/dist/bin/pushary-claude.js +12 -11
- package/dist/bin/pushary-clean.js +8 -7
- package/dist/bin/pushary-codex-bridge.js +5 -5
- package/dist/bin/pushary-codex-hook.js +14 -20
- package/dist/bin/pushary-codex.js +3 -3
- package/dist/bin/pushary-connect.js +10 -10
- package/dist/bin/pushary-daemon.js +6 -6
- package/dist/bin/pushary-disconnect.d.ts +1 -0
- package/dist/bin/pushary-disconnect.js +125 -0
- package/dist/bin/pushary-doctor.js +16 -16
- package/dist/bin/pushary-gemini-bridge.js +5 -5
- package/dist/bin/pushary-gemini-hook.js +14 -20
- package/dist/bin/pushary-hook.js +16 -23
- package/dist/bin/pushary-login.js +6 -6
- package/dist/bin/pushary-logout.js +6 -6
- package/dist/bin/pushary-mode.js +5 -5
- package/dist/bin/pushary-notification-hook.js +8 -11
- package/dist/bin/pushary-permission-denied-hook.js +14 -12
- package/dist/bin/pushary-permission-hook.js +14 -12
- package/dist/bin/pushary-post-hook.js +8 -11
- package/dist/bin/pushary-prompt-hook.js +8 -11
- package/dist/bin/pushary-session-end-hook.js +8 -8
- package/dist/bin/pushary-session-start-hook.js +8 -8
- package/dist/bin/pushary-setup.js +29 -30
- package/dist/bin/pushary-stats.js +4 -4
- package/dist/bin/pushary-status.js +7 -7
- package/dist/bin/pushary-stop-hook.js +8 -8
- package/dist/bin/pushary-stopfailure-hook.js +8 -8
- package/dist/bin/pushary-suggestions.js +2 -2
- package/dist/bin/pushary-upgrade.js +15 -10
- package/dist/bin/pushary-wait.js +5 -5
- package/dist/bin/pushary.js +2 -1
- package/dist/{chunk-ILB6UOAF.js → chunk-55656H4Q.js} +1 -1
- package/dist/chunk-6CUUA7HO.js +59 -0
- package/dist/{chunk-6LC4WSCL.js → chunk-BZKTCOZQ.js} +1 -1
- package/dist/{chunk-K7CSRGKZ.js → chunk-CIVG746V.js} +1 -1
- package/dist/{chunk-FBGBJXVC.js → chunk-DNSIVHIA.js} +1 -1
- package/dist/{chunk-437T77TY.js → chunk-ES3FAX5X.js} +1 -1
- package/dist/chunk-EZPRY2ZY.js +70 -0
- package/dist/{chunk-MQMPG5X4.js → chunk-G3LXA2ZP.js} +207 -10
- package/dist/chunk-GM54KC62.js +187 -0
- package/dist/{chunk-MGJBC6DD.js → chunk-GOEXZ5QJ.js} +1 -0
- package/dist/chunk-HSGEEBBL.js +37 -0
- package/dist/{chunk-UXWEE3QI.js → chunk-IGV4KL7B.js} +1 -1
- package/dist/{chunk-GCKAWHFP.js → chunk-JDQY47OW.js} +1 -1
- package/dist/{chunk-7PZPJSWI.js → chunk-KTKIZEFT.js} +1 -1
- package/dist/{chunk-YR5TSPXB.js → chunk-LB6RSCLL.js} +12 -10
- package/dist/{chunk-DOXSWT4Q.js → chunk-OZKRF7MS.js} +6 -6
- package/dist/{chunk-SEAHGHCE.js → chunk-PHXEADSL.js} +40 -7
- package/dist/{chunk-F52SIIDG.js → chunk-QDKJTPAT.js} +1 -1
- package/dist/{chunk-J4KFVOCQ.js → chunk-SQOFGPNX.js} +1 -1
- package/dist/{chunk-NJ7JT26G.js → chunk-VGADRFG6.js} +1 -1
- package/dist/{chunk-HZNOAJGB.js → chunk-VKJUVPTJ.js} +2 -2
- package/dist/{chunk-ERYKGH4J.js → chunk-XIR6N66V.js} +1 -1
- package/dist/{chunk-CCH775Y6.js → chunk-XLM6COJA.js} +1 -1
- package/dist/{chunk-CCCJ6BPW.js → chunk-Y7BALRVB.js} +73 -134
- package/dist/chunk-YHG74UFF.js +12 -0
- package/dist/src/index.d.ts +7 -22
- package/dist/src/index.js +11 -8
- package/package.json +6 -3
- package/dist/chunk-BLDDXHHA.js +0 -66
- package/dist/chunk-UKSQWTBH.js +0 -208
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseVersion
|
|
3
|
+
} from "./chunk-GM54KC62.js";
|
|
4
|
+
|
|
5
|
+
// src/setup/agent-probe.ts
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
import { readFileSync, realpathSync } from "fs";
|
|
8
|
+
|
|
9
|
+
// src/setup/claude-version.ts
|
|
10
|
+
import { dirname, basename, join } from "path";
|
|
11
|
+
var versionFromInstallPath = (path) => {
|
|
12
|
+
const name = basename(path);
|
|
13
|
+
return /^\d+\.\d+\.\d+$/.test(name) ? parseVersion(name) : null;
|
|
14
|
+
};
|
|
15
|
+
var versionFromManifest = (probe, binPath) => {
|
|
16
|
+
let dir = dirname(binPath);
|
|
17
|
+
for (let depth = 0; depth < 4; depth++) {
|
|
18
|
+
const raw = probe.readFile(join(dir, "package.json"));
|
|
19
|
+
if (raw) {
|
|
20
|
+
try {
|
|
21
|
+
const { version } = JSON.parse(raw);
|
|
22
|
+
if (typeof version === "string") {
|
|
23
|
+
const parsed = parseVersion(version);
|
|
24
|
+
if (parsed) return parsed;
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const parent = dirname(dir);
|
|
30
|
+
if (parent === dir) break;
|
|
31
|
+
dir = parent;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
};
|
|
35
|
+
var readClaudeVersion = (probe) => {
|
|
36
|
+
const onPath2 = probe.resolveOnPath();
|
|
37
|
+
if (!onPath2) return null;
|
|
38
|
+
const resolved = probe.realpath(onPath2) ?? onPath2;
|
|
39
|
+
return versionFromInstallPath(resolved) ?? versionFromManifest(probe, resolved);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// src/setup/agent-probe.ts
|
|
43
|
+
var onPath = (name) => {
|
|
44
|
+
const command = process.platform === "win32" ? "where" : "which";
|
|
45
|
+
try {
|
|
46
|
+
const found = execSync(`${command} ${name}`, { encoding: "utf-8", stdio: "pipe", timeout: 5e3 }).split("\n")[0].trim();
|
|
47
|
+
return found || null;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var realpath = (path) => {
|
|
53
|
+
try {
|
|
54
|
+
return realpathSync(path);
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
var readFile = (path) => {
|
|
60
|
+
try {
|
|
61
|
+
return readFileSync(path, "utf-8");
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var detectClaudeVersion = () => readClaudeVersion({ resolveOnPath: () => onPath("claude"), realpath, readFile });
|
|
67
|
+
|
|
68
|
+
export {
|
|
69
|
+
detectClaudeVersion
|
|
70
|
+
};
|
|
@@ -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,127 @@ 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
|
+
};
|
|
836
1038
|
|
|
837
1039
|
export {
|
|
838
|
-
isApprovalMode,
|
|
839
1040
|
parseAgentQuestionAnswers,
|
|
840
1041
|
normalizeInstallSource,
|
|
841
1042
|
HOOK_BUDGETS,
|
|
@@ -844,21 +1045,17 @@ export {
|
|
|
844
1045
|
effectiveWaitSeconds,
|
|
845
1046
|
normalizeRepoRemote,
|
|
846
1047
|
localRepoKey,
|
|
847
|
-
policyArgForms,
|
|
848
1048
|
isSafeReadOnlyCommand,
|
|
849
1049
|
isAutoApprove,
|
|
850
|
-
isAutoDeny,
|
|
851
|
-
verdictForPolicy,
|
|
852
|
-
selectGoverningRule,
|
|
853
1050
|
isValidApiKey,
|
|
854
1051
|
ACTION_BODY_MAX,
|
|
855
1052
|
DECISION_LINE_MAX,
|
|
856
1053
|
redactSecrets,
|
|
857
1054
|
redactSecretsDeep,
|
|
858
|
-
classifyDecisionRisk,
|
|
859
1055
|
buildDecisionEpisodeFeatures,
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
1056
|
+
resolveAutoResolveOrigin,
|
|
1057
|
+
resolvePolicy,
|
|
1058
|
+
KILL_REASON,
|
|
1059
|
+
resolveGate,
|
|
1060
|
+
modeStateFromResponse
|
|
864
1061
|
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import {
|
|
2
|
+
guardBinary
|
|
3
|
+
} from "./chunk-HSGEEBBL.js";
|
|
4
|
+
import {
|
|
5
|
+
HOOK_BUDGETS
|
|
6
|
+
} from "./chunk-G3LXA2ZP.js";
|
|
7
|
+
|
|
8
|
+
// src/claude-config.ts
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
|
|
11
|
+
// src/agent-version.ts
|
|
12
|
+
var parseVersion = (raw) => {
|
|
13
|
+
const match = raw.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
14
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
|
15
|
+
};
|
|
16
|
+
var compareVersion = (a, b) => {
|
|
17
|
+
for (let index = 0; index < 3; index++) {
|
|
18
|
+
if (a[index] !== b[index]) return a[index] < b[index] ? -1 : 1;
|
|
19
|
+
}
|
|
20
|
+
return 0;
|
|
21
|
+
};
|
|
22
|
+
var atLeast = (version, floor) => compareVersion(version, floor) >= 0;
|
|
23
|
+
|
|
24
|
+
// src/claude-tools.ts
|
|
25
|
+
var PRETOOLUSE_GATED_TOOLS = [
|
|
26
|
+
"Bash",
|
|
27
|
+
"Write",
|
|
28
|
+
"Edit",
|
|
29
|
+
"MultiEdit",
|
|
30
|
+
"NotebookEdit",
|
|
31
|
+
"PowerShell",
|
|
32
|
+
"Monitor"
|
|
33
|
+
];
|
|
34
|
+
var POSTTOOLUSE_MIRRORED_TOOLS = [...PRETOOLUSE_GATED_TOOLS, "TodoWrite"];
|
|
35
|
+
var PRETOOLUSE_HANDLED_TOOLS = /* @__PURE__ */ new Set([...PRETOOLUSE_GATED_TOOLS, "AskUserQuestion"]);
|
|
36
|
+
|
|
37
|
+
// src/claude-events.ts
|
|
38
|
+
var GATED_TOOLS = [...PRETOOLUSE_GATED_TOOLS, "AskUserQuestion"].join("|");
|
|
39
|
+
var MIRRORED_TOOLS = [...PRETOOLUSE_GATED_TOOLS, "TodoWrite"].join("|");
|
|
40
|
+
var ESTABLISHED = [1, 0, 0];
|
|
41
|
+
var OBSERVED = [2, 0, 0];
|
|
42
|
+
var CLAUDE_HOOK_EVENTS = [
|
|
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
|
+
// Observability only. Each carries session shape the island cannot get any other
|
|
54
|
+
// way, and none of them decides anything.
|
|
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
|
+
];
|
|
61
|
+
var supportedClaudeEvents = (version) => CLAUDE_HOOK_EVENTS.filter((hook) => version ? atLeast(version, hook.since) : hook.since === ESTABLISHED);
|
|
62
|
+
|
|
63
|
+
// src/claude-config.ts
|
|
64
|
+
var PUSHARY_MCP_URL = "https://pushary.com/api/mcp/mcp";
|
|
65
|
+
var PUSHARY_PERMISSION_RULE = "mcp__pushary__*";
|
|
66
|
+
var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
67
|
+
var ensureRecord = (target, key) => {
|
|
68
|
+
const existing = asRecord(target[key]);
|
|
69
|
+
if (existing) return existing;
|
|
70
|
+
const created = {};
|
|
71
|
+
target[key] = created;
|
|
72
|
+
return created;
|
|
73
|
+
};
|
|
74
|
+
var isPusharyPermission = (rule) => typeof rule === "string" && (rule.includes("pushary") || rule.includes("MCP(pushary"));
|
|
75
|
+
var HOOK_BINARIES = new Set(CLAUDE_HOOK_EVENTS.map((hook) => hook.binary));
|
|
76
|
+
var namesPusharyBinary = (command) => command.split(/[\s/\\"']+/).some((segment) => HOOK_BINARIES.has(segment.replace(/\.(cmd|exe)$/i, "")));
|
|
77
|
+
var isPusharyHook = (entry2) => {
|
|
78
|
+
const hooks = asRecord(entry2)?.hooks;
|
|
79
|
+
if (!Array.isArray(hooks)) return false;
|
|
80
|
+
return hooks.some((hook) => namesPusharyBinary(String(asRecord(hook)?.command ?? "")));
|
|
81
|
+
};
|
|
82
|
+
var addClaudeMcpServer = (config, apiKey) => {
|
|
83
|
+
const mcpServers = ensureRecord(config, "mcpServers");
|
|
84
|
+
mcpServers.pushary = {
|
|
85
|
+
type: "http",
|
|
86
|
+
url: PUSHARY_MCP_URL,
|
|
87
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
var removeClaudeMcpServers = (config) => {
|
|
91
|
+
let changed = false;
|
|
92
|
+
const removeFrom = (target) => {
|
|
93
|
+
const mcpServers = asRecord(target.mcpServers);
|
|
94
|
+
if (!mcpServers?.pushary) return;
|
|
95
|
+
delete mcpServers.pushary;
|
|
96
|
+
if (Object.keys(mcpServers).length === 0) delete target.mcpServers;
|
|
97
|
+
changed = true;
|
|
98
|
+
};
|
|
99
|
+
removeFrom(config);
|
|
100
|
+
const projects = asRecord(config.projects);
|
|
101
|
+
if (projects) {
|
|
102
|
+
for (const project of Object.values(projects)) {
|
|
103
|
+
const projectConfig = asRecord(project);
|
|
104
|
+
if (projectConfig) removeFrom(projectConfig);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return changed;
|
|
108
|
+
};
|
|
109
|
+
var addPusharyToolPermissions = (settings) => {
|
|
110
|
+
const permissions = ensureRecord(settings, "permissions");
|
|
111
|
+
const allow = Array.isArray(permissions.allow) ? permissions.allow : [];
|
|
112
|
+
const filtered = allow.filter((rule) => !isPusharyPermission(rule));
|
|
113
|
+
if (!filtered.includes(PUSHARY_PERMISSION_RULE)) {
|
|
114
|
+
filtered.push(PUSHARY_PERMISSION_RULE);
|
|
115
|
+
}
|
|
116
|
+
permissions.allow = filtered;
|
|
117
|
+
};
|
|
118
|
+
var addPusharyHooks = (settings, binDir, version = null) => {
|
|
119
|
+
const resolve = (name) => guardBinary(binDir ? join(binDir, name) : name);
|
|
120
|
+
const hooks = ensureRecord(settings, "hooks");
|
|
121
|
+
const supported = new Map(supportedClaudeEvents(version).map((hook) => [hook.event, hook]));
|
|
122
|
+
for (const known of CLAUDE_HOOK_EVENTS) {
|
|
123
|
+
const current = hooks[known.event];
|
|
124
|
+
const others = (Array.isArray(current) ? current : []).filter((item) => !isPusharyHook(item));
|
|
125
|
+
const definition = supported.get(known.event);
|
|
126
|
+
if (definition) {
|
|
127
|
+
hooks[known.event] = [...others, entry(definition, resolve)];
|
|
128
|
+
} else if (others.length > 0) {
|
|
129
|
+
hooks[known.event] = others;
|
|
130
|
+
} else {
|
|
131
|
+
delete hooks[known.event];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
var entry = (definition, resolve) => ({
|
|
136
|
+
...definition.matcher ? { matcher: definition.matcher } : {},
|
|
137
|
+
hooks: [{
|
|
138
|
+
type: "command",
|
|
139
|
+
command: resolve(definition.binary),
|
|
140
|
+
timeout: definition.timeout
|
|
141
|
+
}]
|
|
142
|
+
});
|
|
143
|
+
var removePusharySettings = (settings) => {
|
|
144
|
+
let changed = removeClaudeMcpServers(settings);
|
|
145
|
+
const permissions = asRecord(settings.permissions);
|
|
146
|
+
if (permissions && Array.isArray(permissions.allow)) {
|
|
147
|
+
const filtered = permissions.allow.filter((rule) => !isPusharyPermission(rule));
|
|
148
|
+
if (filtered.length !== permissions.allow.length) {
|
|
149
|
+
if (filtered.length === 0) {
|
|
150
|
+
delete permissions.allow;
|
|
151
|
+
} else {
|
|
152
|
+
permissions.allow = filtered;
|
|
153
|
+
}
|
|
154
|
+
if (Object.keys(permissions).length === 0) delete settings.permissions;
|
|
155
|
+
changed = true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const hooks = asRecord(settings.hooks);
|
|
159
|
+
if (hooks) {
|
|
160
|
+
for (const { event: key } of CLAUDE_HOOK_EVENTS) {
|
|
161
|
+
const entries = hooks[key];
|
|
162
|
+
if (!Array.isArray(entries)) continue;
|
|
163
|
+
const filtered = entries.filter((entry2) => !isPusharyHook(entry2));
|
|
164
|
+
if (filtered.length !== entries.length) {
|
|
165
|
+
if (filtered.length === 0) {
|
|
166
|
+
delete hooks[key];
|
|
167
|
+
} else {
|
|
168
|
+
hooks[key] = filtered;
|
|
169
|
+
}
|
|
170
|
+
changed = true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (Object.keys(hooks).length === 0) delete settings.hooks;
|
|
174
|
+
}
|
|
175
|
+
return changed;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export {
|
|
179
|
+
parseVersion,
|
|
180
|
+
compareVersion,
|
|
181
|
+
PRETOOLUSE_HANDLED_TOOLS,
|
|
182
|
+
addClaudeMcpServer,
|
|
183
|
+
removeClaudeMcpServers,
|
|
184
|
+
addPusharyToolPermissions,
|
|
185
|
+
addPusharyHooks,
|
|
186
|
+
removePusharySettings
|
|
187
|
+
};
|
|
@@ -53,6 +53,7 @@ var COMMAND_LIST = [
|
|
|
53
53
|
]
|
|
54
54
|
},
|
|
55
55
|
{ name: "doctor", module: "pushary-doctor", summary: "Verify your Pushary installation is working" },
|
|
56
|
+
{ name: "disconnect", module: "pushary-disconnect", summary: "Turn off one agent (claude|codex|gemini|cursor)" },
|
|
56
57
|
{ name: "clean", module: "pushary-clean", summary: "Remove all Pushary configuration (--yes for non-interactive)" },
|
|
57
58
|
{ name: "mode", module: "pushary-mode", summary: "Switch approval mode (push_only, push_first, terminal_only)" },
|
|
58
59
|
{ name: "wait", module: "pushary-wait", summary: 'Show or set the "wait for your phone" ladder (pushary wait 45)' },
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
quoteIfNeeded
|
|
3
|
+
} from "./chunk-VT4IVERX.js";
|
|
4
|
+
|
|
5
|
+
// src/hook-command.ts
|
|
6
|
+
var HOOK_SHELL = "/bin/sh -c";
|
|
7
|
+
var LOCATE = 'B=$(command -v "$0") || exit 0; [ -f "$B" ] || exit 0; [ -x "$B" ] || exit 0';
|
|
8
|
+
var NODE_PRESENT = "command -v node >/dev/null 2>&1 || exit 0";
|
|
9
|
+
var HOOK_GUARD_SCRIPTS = {
|
|
10
|
+
native_binary: `${LOCATE}; exec "$B"`,
|
|
11
|
+
node_binary: `${LOCATE}; ${NODE_PRESENT}; exec "$B"`,
|
|
12
|
+
node_script: `[ -f "$0" ] || exit 0; ${NODE_PRESENT}; exec node "$0"`
|
|
13
|
+
};
|
|
14
|
+
var BINARY_GUARDS = {
|
|
15
|
+
native: "native_binary",
|
|
16
|
+
node: "node_binary"
|
|
17
|
+
};
|
|
18
|
+
var guardKindOf = (target) => {
|
|
19
|
+
switch (target.kind) {
|
|
20
|
+
case "node":
|
|
21
|
+
return "node_script";
|
|
22
|
+
case "binary":
|
|
23
|
+
return BINARY_GUARDS[target.runtime];
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var guardHookCommand = (target, platform = process.platform) => {
|
|
27
|
+
const path = quoteIfNeeded(target.kind === "node" ? target.script : target.path);
|
|
28
|
+
if (platform === "win32") return target.kind === "node" ? `node ${path}` : path;
|
|
29
|
+
return `${HOOK_SHELL} '${HOOK_GUARD_SCRIPTS[guardKindOf(target)]}' ${path}`;
|
|
30
|
+
};
|
|
31
|
+
var guardBinary = (path, platform) => guardHookCommand({ kind: "binary", path, runtime: "node" }, platform);
|
|
32
|
+
var guardNodeScript = (script, platform) => guardHookCommand({ kind: "node", script }, platform);
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
guardBinary,
|
|
36
|
+
guardNodeScript
|
|
37
|
+
};
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PRETOOLUSE_HANDLED_TOOLS
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-GM54KC62.js";
|
|
4
4
|
import {
|
|
5
5
|
isGatingMoment,
|
|
6
6
|
recordKeylessMoment
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-VGADRFG6.js";
|
|
8
8
|
import {
|
|
9
9
|
denyReasonFrom,
|
|
10
|
-
isDeferAnswer
|
|
11
|
-
|
|
12
|
-
} from "./chunk-BLDDXHHA.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-
|
|
31
|
+
} from "./chunk-Y7BALRVB.js";
|
|
33
32
|
import {
|
|
34
33
|
getMachineId
|
|
35
34
|
} from "./chunk-RN3NOEJF.js";
|
|
@@ -42,8 +41,9 @@ import {
|
|
|
42
41
|
effectiveWaitSeconds,
|
|
43
42
|
hookWaitClamped,
|
|
44
43
|
hookWaitDeadline,
|
|
45
|
-
parseAgentQuestionAnswers
|
|
46
|
-
|
|
44
|
+
parseAgentQuestionAnswers,
|
|
45
|
+
resolveGate
|
|
46
|
+
} from "./chunk-G3LXA2ZP.js";
|
|
47
47
|
|
|
48
48
|
// src/decision-episode.ts
|
|
49
49
|
var EPISODE_PATH = "/api/agent/decision-episode";
|
|
@@ -513,14 +513,16 @@ var handlePreToolUse = async (input) => {
|
|
|
513
513
|
if (input.tool_name === "AskUserQuestion") {
|
|
514
514
|
return handleAskUserQuestion(apiKey, input);
|
|
515
515
|
}
|
|
516
|
+
if (verdict.kind === "deny") return deny(verdict.reason);
|
|
516
517
|
if (shouldDeferToNativeMode(input.permission_mode, input.tool_name)) {
|
|
517
518
|
return void 0;
|
|
518
519
|
}
|
|
519
520
|
switch (verdict.kind) {
|
|
520
521
|
case "allow":
|
|
521
522
|
return allow();
|
|
522
|
-
|
|
523
|
-
|
|
523
|
+
// No `deny` case: it is handled above the defer, and the compiler enforces
|
|
524
|
+
// that it stays there. Restoring one here would type-error as unreachable.
|
|
525
|
+
//
|
|
524
526
|
// State we could not establish, with an auto-approve on the table. No
|
|
525
527
|
// opinion, so Claude Code's own flow runs as if the hook were absent.
|
|
526
528
|
case "defer":
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
HOOK_BUDGETS
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-G3LXA2ZP.js";
|
|
4
4
|
|
|
5
5
|
// src/gemini-config.ts
|
|
6
6
|
var GEMINI_HOOK_BINARY = "pushary-gemini-hook";
|
|
7
7
|
var GEMINI_MCP_URL = "https://pushary.com/api/mcp/mcp";
|
|
8
8
|
var GEMINI_HOOK_EVENTS = [
|
|
9
|
-
{ event: "BeforeTool", matcher: "run_shell_command|write_file|replace", timeoutMs: HOOK_BUDGETS.gemini.budgetSeconds * 1e3 },
|
|
10
|
-
{ event: "AfterTool", matcher: "run_shell_command|write_file|replace", timeoutMs: 1e4 },
|
|
11
|
-
{ event: "BeforeAgent", timeoutMs: 1e4 },
|
|
12
|
-
{ event: "SessionStart", timeoutMs: 1e4 },
|
|
13
|
-
{ event: "SessionEnd", timeoutMs: 1e4 }
|
|
9
|
+
{ event: "BeforeTool", matcher: "run_shell_command|write_file|replace", delivery: "approval", timeoutMs: HOOK_BUDGETS.gemini.budgetSeconds * 1e3 },
|
|
10
|
+
{ event: "AfterTool", matcher: "run_shell_command|write_file|replace", delivery: "telemetry", timeoutMs: 1e4 },
|
|
11
|
+
{ event: "BeforeAgent", delivery: "telemetry", timeoutMs: 1e4 },
|
|
12
|
+
{ event: "SessionStart", delivery: "telemetry", timeoutMs: 1e4 },
|
|
13
|
+
{ event: "SessionEnd", delivery: "telemetry", timeoutMs: 1e4 }
|
|
14
14
|
];
|
|
15
15
|
var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
16
16
|
var ensureRecord = (target, key) => {
|