@pushary/agent-hooks 0.68.0 → 0.70.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/CHANGELOG.md +138 -0
- package/README.md +3 -2
- package/data/cursor-plugin/scripts/pushary-gate.mjs +70 -15
- package/data/vscode-plugin/scripts/pushary-gate.mjs +59 -11
- package/dist/bin/pushary-claude.js +21 -11
- package/dist/bin/pushary-clean.js +60 -22
- package/dist/bin/pushary-codex-hook.js +38 -23
- package/dist/bin/pushary-codex.js +1 -1
- package/dist/bin/pushary-connect.js +6 -4
- package/dist/bin/pushary-daemon.js +3 -3
- package/dist/bin/pushary-doctor.js +120 -28
- package/dist/bin/pushary-gemini-hook.js +32 -16
- package/dist/bin/pushary-hook.js +5 -5
- package/dist/bin/pushary-login.js +2 -2
- package/dist/bin/pushary-logout.js +2 -2
- package/dist/bin/pushary-mode.js +2 -2
- package/dist/bin/pushary-notification-hook.js +1 -1
- package/dist/bin/pushary-permission-denied-hook.js +3 -3
- package/dist/bin/pushary-permission-hook.js +3 -3
- package/dist/bin/pushary-post-hook.js +1 -1
- package/dist/bin/pushary-prompt-hook.js +1 -1
- package/dist/bin/pushary-session-end-hook.js +1 -1
- package/dist/bin/pushary-session-start-hook.js +1 -1
- package/dist/bin/pushary-setup.js +90 -13
- package/dist/bin/pushary-stats.js +2 -2
- package/dist/bin/pushary-status.js +4 -4
- package/dist/bin/pushary-stop-hook.js +1 -1
- package/dist/bin/pushary-stopfailure-hook.js +1 -1
- package/dist/bin/pushary-suggestions.js +2 -2
- package/dist/bin/pushary-upgrade.js +2 -2
- package/dist/bin/pushary-wait.js +2 -2
- package/dist/bin/pushary.js +1 -1
- package/dist/{chunk-JGH37QRB.js → chunk-7OYGFJYZ.js} +1 -1
- package/dist/chunk-B7R4YSAB.js +38 -0
- package/dist/{chunk-ATP6UQPH.js → chunk-BBK3TTU2.js} +1 -1
- package/dist/{chunk-XOUYM27W.js → chunk-FZIQSDTQ.js} +207 -113
- package/dist/chunk-GD275GEX.js +66 -0
- package/dist/{chunk-SONXGCZY.js → chunk-HU43EZ5Q.js} +45 -27
- package/dist/{chunk-KB4ODLGB.js → chunk-I4BYZIX4.js} +2 -0
- package/dist/{chunk-YMFZUXL5.js → chunk-MGFZXUUR.js} +57 -4
- package/dist/{chunk-YZT2CBC3.js → chunk-NQTRA7H5.js} +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +3 -3
- package/package.json +1 -1
- package/dist/chunk-YHG74UFF.js +0 -12
|
@@ -54,6 +54,11 @@ var isWaitForAnswerResponse = (data) => {
|
|
|
54
54
|
|
|
55
55
|
// src/policy.ts
|
|
56
56
|
var CACHE_TTL_MS = 60 * 1e3;
|
|
57
|
+
var STALE_CACHE_MAX_MS = 12 * 60 * 60 * 1e3;
|
|
58
|
+
var policyRequestFor = (agentType) => ({
|
|
59
|
+
agent: agentType,
|
|
60
|
+
repoAware: agentType === "claude_code"
|
|
61
|
+
});
|
|
57
62
|
var policyCacheFile = (apiKey, agent, repoAware = false) => {
|
|
58
63
|
const parts = [apiKey, agent, repoAware ? "repoAware" : void 0].filter(Boolean);
|
|
59
64
|
const hash = createHash("sha256").update(parts.join(":")).digest("hex").slice(0, 12);
|
|
@@ -88,18 +93,19 @@ var getPolicy = async (apiKey, expectedVersion, agent, repoAware = false) => {
|
|
|
88
93
|
const cached = JSON.parse(stat);
|
|
89
94
|
if (!isPolicyConfig(cached)) throw new Error("Corrupted cache");
|
|
90
95
|
const versionStale = expectedVersion != null && (cached._policyVersion ?? null) !== expectedVersion;
|
|
91
|
-
const
|
|
96
|
+
const age = cached._cachedAt ? Date.now() - cached._cachedAt : 0;
|
|
97
|
+
const ttlFresh = !cached._cachedAt || age < CACHE_TTL_MS;
|
|
92
98
|
if (ttlFresh && !versionStale) {
|
|
93
99
|
return cached;
|
|
94
100
|
}
|
|
95
|
-
staleCache = cached;
|
|
101
|
+
if (age < STALE_CACHE_MAX_MS) staleCache = cached;
|
|
96
102
|
} catch {
|
|
97
103
|
}
|
|
98
104
|
}
|
|
99
105
|
try {
|
|
100
106
|
const policy = await fetchPolicy(apiKey, agent, repoAware);
|
|
101
107
|
try {
|
|
102
|
-
writeFileSync(path, JSON.stringify({ ...policy, _cachedAt: Date.now(), _policyVersion: expectedVersion ?? null }), "utf-8");
|
|
108
|
+
writeFileSync(path, JSON.stringify({ ...policy, _cachedAt: Date.now(), _policyVersion: expectedVersion ?? null }), { encoding: "utf-8", mode: 384 });
|
|
103
109
|
} catch {
|
|
104
110
|
}
|
|
105
111
|
return policy;
|
|
@@ -130,6 +136,8 @@ var findBestMatch = (policies, toolName, args, repoKey) => {
|
|
|
130
136
|
}
|
|
131
137
|
return best;
|
|
132
138
|
};
|
|
139
|
+
var isAutoApprove = (policy) => policy.timeoutSeconds === 0 && policy.timeoutAction === "approve";
|
|
140
|
+
var isAutoDeny = (policy) => policy.timeoutSeconds === 0 && policy.timeoutAction === "deny";
|
|
133
141
|
var autoApprove = (tool) => ({
|
|
134
142
|
tool,
|
|
135
143
|
timeoutSeconds: 0,
|
|
@@ -162,7 +170,7 @@ var resolvePolicy = (config, toolName, modeOverride, toolInput, cwd, repoKey) =>
|
|
|
162
170
|
if (!governedBySpecificRule && toolName === "Bash" && typeof arg === "string" && isSafeReadOnlyCommand(arg)) {
|
|
163
171
|
base = autoApprove(base.tool);
|
|
164
172
|
}
|
|
165
|
-
const wouldAutoApprove = base
|
|
173
|
+
const wouldAutoApprove = isAutoApprove(base);
|
|
166
174
|
if (!governedBySpecificRule && wouldAutoApprove && toolName === "Bash" && typeof arg === "string" && classifyDecisionRisk(toolName, arg) === "destructive") {
|
|
167
175
|
base = {
|
|
168
176
|
tool: base.tool,
|
|
@@ -178,6 +186,57 @@ var resolvePolicy = (config, toolName, modeOverride, toolInput, cwd, repoKey) =>
|
|
|
178
186
|
}
|
|
179
187
|
return base;
|
|
180
188
|
};
|
|
189
|
+
var restraintRank = (policy) => {
|
|
190
|
+
if (isAutoDeny(policy)) return 4;
|
|
191
|
+
if (isAutoApprove(policy)) return 0;
|
|
192
|
+
if (policy.mode === "push_only") return 3;
|
|
193
|
+
return policy.mode === "terminal_only" || policy.mode === "notify_only" ? 1 : 2;
|
|
194
|
+
};
|
|
195
|
+
var strictestPolicy = (a, b) => restraintRank(b) > restraintRank(a) ? b : a;
|
|
196
|
+
var resolvePolicyAcross = (config, toolName, modeOverride, toolInputs, cwd, repoKey) => {
|
|
197
|
+
if (toolInputs.length === 0) return resolvePolicy(config, toolName, modeOverride, void 0, cwd, repoKey);
|
|
198
|
+
return toolInputs.map((input) => resolvePolicy(config, toolName, modeOverride, input, cwd, repoKey)).reduce(strictestPolicy);
|
|
199
|
+
};
|
|
200
|
+
var isModeStateDegraded = (state) => state?.degraded === true;
|
|
201
|
+
var MODE_GRACE_MS = 60 * 1e3;
|
|
202
|
+
var modeStateCacheFile = (apiKey) => {
|
|
203
|
+
const hash = createHash("sha256").update(`mode:${apiKey}`).digest("hex").slice(0, 12);
|
|
204
|
+
return join(tmpdir(), `pushary-mode-${hash}.json`);
|
|
205
|
+
};
|
|
206
|
+
var readLastGoodMode = (apiKey) => {
|
|
207
|
+
try {
|
|
208
|
+
const path = modeStateCacheFile(apiKey);
|
|
209
|
+
if (!existsSync(path)) return null;
|
|
210
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
211
|
+
if (!parsed || typeof parsed.at !== "number" || !parsed.state) return null;
|
|
212
|
+
return parsed;
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
var writeLastGoodMode = (apiKey, state) => {
|
|
218
|
+
try {
|
|
219
|
+
writeFileSync(modeStateCacheFile(apiKey), JSON.stringify({ state, at: Date.now() }), { encoding: "utf-8", mode: 384 });
|
|
220
|
+
} catch {
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
var unknownMode = () => ({
|
|
224
|
+
mode: null,
|
|
225
|
+
kill: false,
|
|
226
|
+
policyVersion: null,
|
|
227
|
+
relayUrl: null,
|
|
228
|
+
trainingConsent: false,
|
|
229
|
+
scope: null,
|
|
230
|
+
degraded: true
|
|
231
|
+
});
|
|
232
|
+
var degradedFallback = (apiKey, authoritative = false) => {
|
|
233
|
+
if (authoritative) return unknownMode();
|
|
234
|
+
const last = readLastGoodMode(apiKey);
|
|
235
|
+
if (last && Date.now() - last.at < MODE_GRACE_MS) {
|
|
236
|
+
return { ...last.state, degraded: false };
|
|
237
|
+
}
|
|
238
|
+
return unknownMode();
|
|
239
|
+
};
|
|
181
240
|
var toPolicyVersion = (value) => typeof value === "string" || typeof value === "number" ? String(value) : null;
|
|
182
241
|
var fetchModeState = async (apiKey, sessionId) => {
|
|
183
242
|
try {
|
|
@@ -187,10 +246,12 @@ var fetchModeState = async (apiKey, sessionId) => {
|
|
|
187
246
|
headers: { "Authorization": `Bearer ${apiKey}` },
|
|
188
247
|
signal: AbortSignal.timeout(3e3)
|
|
189
248
|
});
|
|
190
|
-
if (!response.ok)
|
|
249
|
+
if (!response.ok) {
|
|
250
|
+
return degradedFallback(apiKey, response.status === 401 || response.status === 403);
|
|
251
|
+
}
|
|
191
252
|
const data = await response.json();
|
|
192
253
|
const mode = data.override?.mode;
|
|
193
|
-
|
|
254
|
+
const state = {
|
|
194
255
|
mode: isApprovalMode(mode) ? mode : null,
|
|
195
256
|
kill: data.kill === true,
|
|
196
257
|
policyVersion: toPolicyVersion(data.policyVersion),
|
|
@@ -198,10 +259,13 @@ var fetchModeState = async (apiKey, sessionId) => {
|
|
|
198
259
|
trainingConsent: data.trainingConsent === true,
|
|
199
260
|
// Validated rather than cast: a malformed contract must read as no contract,
|
|
200
261
|
// never as one that silently matches nothing.
|
|
201
|
-
scope: isScopeContract(data.scope) ? data.scope : null
|
|
262
|
+
scope: isScopeContract(data.scope) ? data.scope : null,
|
|
263
|
+
degraded: false
|
|
202
264
|
};
|
|
265
|
+
writeLastGoodMode(apiKey, state);
|
|
266
|
+
return state;
|
|
203
267
|
} catch {
|
|
204
|
-
return
|
|
268
|
+
return degradedFallback(apiKey);
|
|
205
269
|
}
|
|
206
270
|
};
|
|
207
271
|
var fetchModeOverride = async (apiKey) => (await fetchModeState(apiKey)).mode;
|
|
@@ -275,7 +339,7 @@ var TOOL_TARGET_MAX_LENGTH = 80;
|
|
|
275
339
|
var deriveCommandHead = (command) => {
|
|
276
340
|
if (typeof command !== "string") return void 0;
|
|
277
341
|
const head = command.trim().split(/\s+/).slice(0, 2).join(" ");
|
|
278
|
-
return head ? head.slice(0, TOOL_TARGET_MAX_LENGTH) : void 0;
|
|
342
|
+
return head ? redactSecrets(head).slice(0, TOOL_TARGET_MAX_LENGTH) : void 0;
|
|
279
343
|
};
|
|
280
344
|
var deriveToolTarget = (toolName, toolInput) => {
|
|
281
345
|
if (toolName === "Bash" || toolName === "PowerShell" || toolName === "Monitor") {
|
|
@@ -300,7 +364,7 @@ var deriveReceiptCommandHead = (command) => {
|
|
|
300
364
|
const [first, second] = tokens;
|
|
301
365
|
const keep = first && second && SCRIPT_RUNNERS.has(first.toLowerCase()) && RUN_SUBCOMMANDS.has(second.toLowerCase()) ? 3 : 2;
|
|
302
366
|
const head = tokens.slice(0, keep).join(" ");
|
|
303
|
-
return head ? head.slice(0, TOOL_TARGET_MAX_LENGTH) : void 0;
|
|
367
|
+
return head ? redactSecrets(head).slice(0, TOOL_TARGET_MAX_LENGTH) : void 0;
|
|
304
368
|
};
|
|
305
369
|
var RECEIPT_TARGET_MAX_LENGTH = 256;
|
|
306
370
|
var EXIT_CODE_FAILURE = /^Error: Exit code \d+/;
|
|
@@ -335,7 +399,9 @@ var deriveReceiptMeta = (toolName, toolInput, toolResult, cwd) => {
|
|
|
335
399
|
if (typeof filePath !== "string" || !filePath) return void 0;
|
|
336
400
|
return {
|
|
337
401
|
kind: toolName === "Write" ? "write" : "edit",
|
|
338
|
-
|
|
402
|
+
// A path is an unlikely place for a credential but not an impossible one
|
|
403
|
+
// (a token in a temp directory name), and this field is persisted.
|
|
404
|
+
target: redactSecrets(relativizeReceiptPath(filePath, cwd)).slice(0, RECEIPT_TARGET_MAX_LENGTH),
|
|
339
405
|
ok
|
|
340
406
|
};
|
|
341
407
|
}
|
|
@@ -441,9 +507,93 @@ var removePendingSession = (sessionId) => {
|
|
|
441
507
|
}
|
|
442
508
|
};
|
|
443
509
|
|
|
510
|
+
// src/repo.ts
|
|
511
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
512
|
+
import { basename, dirname, isAbsolute as isAbsolute2, join as join3, resolve } from "path";
|
|
513
|
+
var MAX_PARENT_WALK = 64;
|
|
514
|
+
var GIT_FILE_PREFIX = "gitdir:";
|
|
515
|
+
var findGitDir = (startDir) => {
|
|
516
|
+
let current = resolve(startDir);
|
|
517
|
+
for (let depth = 0; depth < MAX_PARENT_WALK; depth += 1) {
|
|
518
|
+
const candidate = join3(current, ".git");
|
|
519
|
+
try {
|
|
520
|
+
if (existsSync3(candidate)) {
|
|
521
|
+
if (statSync2(candidate).isDirectory()) return candidate;
|
|
522
|
+
const pointer = readFileSync2(candidate, "utf-8").trim();
|
|
523
|
+
if (pointer.startsWith(GIT_FILE_PREFIX)) {
|
|
524
|
+
const target = pointer.slice(GIT_FILE_PREFIX.length).trim();
|
|
525
|
+
return isAbsolute2(target) ? target : resolve(current, target);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
} catch {
|
|
529
|
+
}
|
|
530
|
+
const parent = dirname(current);
|
|
531
|
+
if (parent === current) return void 0;
|
|
532
|
+
current = parent;
|
|
533
|
+
}
|
|
534
|
+
return void 0;
|
|
535
|
+
};
|
|
536
|
+
var configPathFor = (gitDir) => {
|
|
537
|
+
const index = gitDir.replace(/\\/g, "/").indexOf("/worktrees/");
|
|
538
|
+
return index === -1 ? join3(gitDir, "config") : join3(gitDir.slice(0, index), "config");
|
|
539
|
+
};
|
|
540
|
+
var ORIGIN_SECTION = /^\[remote "origin"\]/;
|
|
541
|
+
var SECTION_HEADER = /^\[/;
|
|
542
|
+
var URL_LINE = /^url\s*=\s*(.+)$/;
|
|
543
|
+
var readOriginRemote = (gitDir) => {
|
|
544
|
+
try {
|
|
545
|
+
const configPath = configPathFor(gitDir);
|
|
546
|
+
if (!existsSync3(configPath)) return void 0;
|
|
547
|
+
let inOrigin = false;
|
|
548
|
+
for (const rawLine of readFileSync2(configPath, "utf-8").split("\n")) {
|
|
549
|
+
const line = rawLine.trim();
|
|
550
|
+
if (SECTION_HEADER.test(line)) {
|
|
551
|
+
inOrigin = ORIGIN_SECTION.test(line);
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
if (!inOrigin) continue;
|
|
555
|
+
const url = URL_LINE.exec(line);
|
|
556
|
+
if (url) return url[1]?.trim();
|
|
557
|
+
}
|
|
558
|
+
} catch {
|
|
559
|
+
}
|
|
560
|
+
return void 0;
|
|
561
|
+
};
|
|
562
|
+
var deriveRepoKey = (cwd) => {
|
|
563
|
+
const dir = cwd && cwd.length > 0 ? cwd : process.cwd();
|
|
564
|
+
try {
|
|
565
|
+
const gitDir = findGitDir(dir);
|
|
566
|
+
if (gitDir) {
|
|
567
|
+
const remote = readOriginRemote(gitDir);
|
|
568
|
+
const normalized = remote ? normalizeRepoRemote(remote) : void 0;
|
|
569
|
+
if (normalized) return normalized;
|
|
570
|
+
const root = gitDir.endsWith(".git") ? dirname(gitDir) : dir;
|
|
571
|
+
return localRepoKey(basename(root));
|
|
572
|
+
}
|
|
573
|
+
return localRepoKey(basename(resolve(dir)));
|
|
574
|
+
} catch {
|
|
575
|
+
return void 0;
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
var repoKeyCache = /* @__PURE__ */ new Map();
|
|
579
|
+
var REPO_KEY_ENV = "PUSHARY_REPO_KEY";
|
|
580
|
+
var repoKeyFor = (cwd) => {
|
|
581
|
+
const override = process.env[REPO_KEY_ENV];
|
|
582
|
+
if (typeof override === "string") {
|
|
583
|
+
const trimmed = override.trim();
|
|
584
|
+
if (trimmed.toLowerCase() === "off") return void 0;
|
|
585
|
+
if (trimmed.length > 0) return trimmed.toLowerCase();
|
|
586
|
+
}
|
|
587
|
+
const dir = cwd && cwd.length > 0 ? cwd : process.cwd();
|
|
588
|
+
if (repoKeyCache.has(dir)) return repoKeyCache.get(dir);
|
|
589
|
+
const derived = deriveRepoKey(dir);
|
|
590
|
+
repoKeyCache.set(dir, derived);
|
|
591
|
+
return derived;
|
|
592
|
+
};
|
|
593
|
+
|
|
444
594
|
// src/usage.ts
|
|
445
|
-
import { closeSync, mkdirSync as mkdirSync2, openSync, readFileSync as
|
|
446
|
-
import { join as
|
|
595
|
+
import { closeSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync3, readSync, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
596
|
+
import { join as join4 } from "path";
|
|
447
597
|
import { tmpdir as tmpdir3 } from "os";
|
|
448
598
|
var DEFAULT_PRICES = [
|
|
449
599
|
{ match: "opus-4-1", in: 15, out: 75 },
|
|
@@ -479,12 +629,12 @@ var estimateCostUsd = (usage, model) => {
|
|
|
479
629
|
const perTokenOut = price.out / 1e6;
|
|
480
630
|
return usage.inputTokens * perTokenIn + usage.outputTokens * perTokenOut + usage.cacheCreationTokens * perTokenIn * CACHE_WRITE_MULTIPLIER + usage.cacheReadTokens * perTokenIn * CACHE_READ_MULTIPLIER;
|
|
481
631
|
};
|
|
482
|
-
var stateDir = () => process.env.PUSHARY_USAGE_DIR?.trim() ||
|
|
483
|
-
var stateFile = (sessionId) =>
|
|
632
|
+
var stateDir = () => process.env.PUSHARY_USAGE_DIR?.trim() || join4(tmpdir3(), "pushary-usage");
|
|
633
|
+
var stateFile = (sessionId) => join4(stateDir(), sessionId.replace(/[^a-zA-Z0-9_-]/g, "_"));
|
|
484
634
|
var emptyState = () => ({ offset: 0, tokensIn: 0, tokensOut: 0, costUsd: 0, recentIds: [] });
|
|
485
635
|
var readState = (path) => {
|
|
486
636
|
try {
|
|
487
|
-
const parsed = JSON.parse(
|
|
637
|
+
const parsed = JSON.parse(readFileSync3(path, "utf-8"));
|
|
488
638
|
if (typeof parsed.offset === "number" && parsed.offset >= 0 && typeof parsed.tokensIn === "number" && typeof parsed.tokensOut === "number" && typeof parsed.costUsd === "number" && Array.isArray(parsed.recentIds)) {
|
|
489
639
|
return {
|
|
490
640
|
offset: parsed.offset,
|
|
@@ -559,7 +709,7 @@ var extractUserText = (content) => {
|
|
|
559
709
|
};
|
|
560
710
|
var readLastUserPrompt = (transcriptPath) => {
|
|
561
711
|
try {
|
|
562
|
-
const size =
|
|
712
|
+
const size = statSync3(transcriptPath).size;
|
|
563
713
|
const start = Math.max(0, size - USER_PROMPT_TAIL_BYTES);
|
|
564
714
|
const lines = readRange(transcriptPath, start, size).toString("utf-8").split("\n");
|
|
565
715
|
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
@@ -582,7 +732,7 @@ var readLastUserPrompt = (transcriptPath) => {
|
|
|
582
732
|
};
|
|
583
733
|
var readNewUsage = (transcriptPath, sessionId) => {
|
|
584
734
|
try {
|
|
585
|
-
const size =
|
|
735
|
+
const size = statSync3(transcriptPath).size;
|
|
586
736
|
const path = stateFile(sessionId);
|
|
587
737
|
let state = readState(path);
|
|
588
738
|
if (size < state.offset) state = { ...emptyState(), recentIds: state.recentIds };
|
|
@@ -613,7 +763,7 @@ var readNewUsage = (transcriptPath, sessionId) => {
|
|
|
613
763
|
};
|
|
614
764
|
|
|
615
765
|
// src/codex-adapter.ts
|
|
616
|
-
import { resolve } from "path";
|
|
766
|
+
import { resolve as resolve2 } from "path";
|
|
617
767
|
var CODEX_AGENT = { type: "codex", label: "Codex" };
|
|
618
768
|
var codexAllow = () => ({ kind: "allow" });
|
|
619
769
|
var codexDeny = (reason) => ({ kind: "deny", reason });
|
|
@@ -679,10 +829,25 @@ var toPolicyLookup = (toolName, toolInput, cwd) => {
|
|
|
679
829
|
const command = toolInput.command;
|
|
680
830
|
const files = parseApplyPatchFiles(command);
|
|
681
831
|
if (files.length === 1) {
|
|
682
|
-
return { tool: "Edit", input: { file_path: cwd ?
|
|
832
|
+
return { tool: "Edit", input: { file_path: cwd ? resolve2(cwd, files[0]) : files[0] } };
|
|
683
833
|
}
|
|
684
834
|
return { tool: "Edit", input: typeof command === "string" ? { file_path: command } : {} };
|
|
685
835
|
};
|
|
836
|
+
var toPolicyLookups = (toolName, toolInput, cwd) => {
|
|
837
|
+
if (toolName !== "apply_patch") {
|
|
838
|
+
const single = toPolicyLookup(toolName, toolInput, cwd);
|
|
839
|
+
return { tool: single.tool, inputs: [single.input] };
|
|
840
|
+
}
|
|
841
|
+
const files = parseApplyPatchFiles(toolInput.command);
|
|
842
|
+
if (files.length === 0) {
|
|
843
|
+
const single = toPolicyLookup(toolName, toolInput, cwd);
|
|
844
|
+
return { tool: single.tool, inputs: [single.input] };
|
|
845
|
+
}
|
|
846
|
+
return {
|
|
847
|
+
tool: "Edit",
|
|
848
|
+
inputs: files.map((file) => ({ file_path: cwd ? resolve2(cwd, file) : file }))
|
|
849
|
+
};
|
|
850
|
+
};
|
|
686
851
|
var permissionTimeoutDecision = (timeoutAction) => {
|
|
687
852
|
if (timeoutAction === "approve") return codexAllow();
|
|
688
853
|
if (timeoutAction === "deny") return codexDeny("No response within timeout");
|
|
@@ -690,90 +855,6 @@ var permissionTimeoutDecision = (timeoutAction) => {
|
|
|
690
855
|
};
|
|
691
856
|
var preToolUseTimeoutDecision = (timeoutAction, denyReason = "No response within timeout") => timeoutAction === "deny" ? codexDeny(denyReason) : codexPass();
|
|
692
857
|
|
|
693
|
-
// src/repo.ts
|
|
694
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
|
|
695
|
-
import { basename, dirname, isAbsolute as isAbsolute2, join as join4, resolve as resolve2 } from "path";
|
|
696
|
-
var MAX_PARENT_WALK = 64;
|
|
697
|
-
var GIT_FILE_PREFIX = "gitdir:";
|
|
698
|
-
var findGitDir = (startDir) => {
|
|
699
|
-
let current = resolve2(startDir);
|
|
700
|
-
for (let depth = 0; depth < MAX_PARENT_WALK; depth += 1) {
|
|
701
|
-
const candidate = join4(current, ".git");
|
|
702
|
-
try {
|
|
703
|
-
if (existsSync3(candidate)) {
|
|
704
|
-
if (statSync3(candidate).isDirectory()) return candidate;
|
|
705
|
-
const pointer = readFileSync3(candidate, "utf-8").trim();
|
|
706
|
-
if (pointer.startsWith(GIT_FILE_PREFIX)) {
|
|
707
|
-
const target = pointer.slice(GIT_FILE_PREFIX.length).trim();
|
|
708
|
-
return isAbsolute2(target) ? target : resolve2(current, target);
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
} catch {
|
|
712
|
-
}
|
|
713
|
-
const parent = dirname(current);
|
|
714
|
-
if (parent === current) return void 0;
|
|
715
|
-
current = parent;
|
|
716
|
-
}
|
|
717
|
-
return void 0;
|
|
718
|
-
};
|
|
719
|
-
var configPathFor = (gitDir) => {
|
|
720
|
-
const index = gitDir.replace(/\\/g, "/").indexOf("/worktrees/");
|
|
721
|
-
return index === -1 ? join4(gitDir, "config") : join4(gitDir.slice(0, index), "config");
|
|
722
|
-
};
|
|
723
|
-
var ORIGIN_SECTION = /^\[remote "origin"\]/;
|
|
724
|
-
var SECTION_HEADER = /^\[/;
|
|
725
|
-
var URL_LINE = /^url\s*=\s*(.+)$/;
|
|
726
|
-
var readOriginRemote = (gitDir) => {
|
|
727
|
-
try {
|
|
728
|
-
const configPath = configPathFor(gitDir);
|
|
729
|
-
if (!existsSync3(configPath)) return void 0;
|
|
730
|
-
let inOrigin = false;
|
|
731
|
-
for (const rawLine of readFileSync3(configPath, "utf-8").split("\n")) {
|
|
732
|
-
const line = rawLine.trim();
|
|
733
|
-
if (SECTION_HEADER.test(line)) {
|
|
734
|
-
inOrigin = ORIGIN_SECTION.test(line);
|
|
735
|
-
continue;
|
|
736
|
-
}
|
|
737
|
-
if (!inOrigin) continue;
|
|
738
|
-
const url = URL_LINE.exec(line);
|
|
739
|
-
if (url) return url[1]?.trim();
|
|
740
|
-
}
|
|
741
|
-
} catch {
|
|
742
|
-
}
|
|
743
|
-
return void 0;
|
|
744
|
-
};
|
|
745
|
-
var deriveRepoKey = (cwd) => {
|
|
746
|
-
const dir = cwd && cwd.length > 0 ? cwd : process.cwd();
|
|
747
|
-
try {
|
|
748
|
-
const gitDir = findGitDir(dir);
|
|
749
|
-
if (gitDir) {
|
|
750
|
-
const remote = readOriginRemote(gitDir);
|
|
751
|
-
const normalized = remote ? normalizeRepoRemote(remote) : void 0;
|
|
752
|
-
if (normalized) return normalized;
|
|
753
|
-
const root = gitDir.endsWith(".git") ? dirname(gitDir) : dir;
|
|
754
|
-
return localRepoKey(basename(root));
|
|
755
|
-
}
|
|
756
|
-
return localRepoKey(basename(resolve2(dir)));
|
|
757
|
-
} catch {
|
|
758
|
-
return void 0;
|
|
759
|
-
}
|
|
760
|
-
};
|
|
761
|
-
var repoKeyCache = /* @__PURE__ */ new Map();
|
|
762
|
-
var REPO_KEY_ENV = "PUSHARY_REPO_KEY";
|
|
763
|
-
var repoKeyFor = (cwd) => {
|
|
764
|
-
const override = process.env[REPO_KEY_ENV];
|
|
765
|
-
if (typeof override === "string") {
|
|
766
|
-
const trimmed = override.trim();
|
|
767
|
-
if (trimmed.toLowerCase() === "off") return void 0;
|
|
768
|
-
if (trimmed.length > 0) return trimmed.toLowerCase();
|
|
769
|
-
}
|
|
770
|
-
const dir = cwd && cwd.length > 0 ? cwd : process.cwd();
|
|
771
|
-
if (repoKeyCache.has(dir)) return repoKeyCache.get(dir);
|
|
772
|
-
const derived = deriveRepoKey(dir);
|
|
773
|
-
repoKeyCache.set(dir, derived);
|
|
774
|
-
return derived;
|
|
775
|
-
};
|
|
776
|
-
|
|
777
858
|
// src/throttle.ts
|
|
778
859
|
import { join as join5 } from "path";
|
|
779
860
|
import { tmpdir as tmpdir4 } from "os";
|
|
@@ -870,31 +951,38 @@ var notifyLateAnswers = async (apiKey, late, agentName, sessionId) => {
|
|
|
870
951
|
};
|
|
871
952
|
var CLAUDE_CODE_AGENT = { type: "claude_code", label: "Claude Code" };
|
|
872
953
|
var POLICY_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
873
|
-
var readFreshCachedPolicy = (apiKey) => {
|
|
874
|
-
const
|
|
954
|
+
var readFreshCachedPolicy = (apiKey, agentType) => {
|
|
955
|
+
const request = policyRequestFor(agentType);
|
|
956
|
+
const path = policyCacheFile(apiKey, request.agent, request.repoAware);
|
|
875
957
|
if (!existsSync5(path)) return null;
|
|
876
958
|
const cached = JSON.parse(readFileSync4(path, "utf-8"));
|
|
877
959
|
if (!isPolicyConfig(cached)) return null;
|
|
878
960
|
if (!cached._cachedAt || Date.now() - cached._cachedAt >= POLICY_CACHE_TTL_MS) return null;
|
|
879
961
|
return cached;
|
|
880
962
|
};
|
|
881
|
-
var
|
|
963
|
+
var autoDecisionEnabled = () => {
|
|
964
|
+
const flag = process.env.PUSHARY_AUTO_DECISION_BEACON;
|
|
965
|
+
return flag === "1" || flag === "true";
|
|
966
|
+
};
|
|
967
|
+
var deriveDecisionSource = (toolName, toolInput, liveMode, agentType, cwd) => {
|
|
882
968
|
try {
|
|
883
969
|
if (liveMode.kill) return "terminal";
|
|
884
|
-
|
|
970
|
+
if (!autoDecisionEnabled()) return void 0;
|
|
971
|
+
const policy = readFreshCachedPolicy(getApiKey(), agentType);
|
|
885
972
|
if (!policy) return void 0;
|
|
886
973
|
const resolved = resolvePolicy(policy, toolName, liveMode.mode, toolInput, cwd, repoKeyFor(cwd));
|
|
887
|
-
if (resolved
|
|
974
|
+
if (isAutoApprove(resolved)) return "policy_auto";
|
|
888
975
|
if (resolved.mode === "push_only" || resolved.mode === "push_first") return "human";
|
|
889
976
|
return "terminal";
|
|
890
977
|
} catch {
|
|
891
978
|
return void 0;
|
|
892
979
|
}
|
|
893
980
|
};
|
|
894
|
-
var deriveAutoDecisionBeacon = (toolName, toolInput, liveMode, cwd) => {
|
|
981
|
+
var deriveAutoDecisionBeacon = (toolName, toolInput, liveMode, agentType, cwd) => {
|
|
895
982
|
try {
|
|
896
983
|
if (liveMode.kill) return void 0;
|
|
897
|
-
|
|
984
|
+
if (!autoDecisionEnabled()) return void 0;
|
|
985
|
+
const policy = readFreshCachedPolicy(getApiKey(), agentType);
|
|
898
986
|
if (!policy) return void 0;
|
|
899
987
|
const resolved = resolvePolicy(policy, toolName, liveMode.mode, toolInput, cwd, repoKeyFor(cwd));
|
|
900
988
|
if (resolved.timeoutSeconds !== 0 || resolved.timeoutAction !== "approve") return void 0;
|
|
@@ -980,8 +1068,8 @@ var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
|
|
|
980
1068
|
for (const l of late) removePendingQuestion(sessionKey, l.correlationId);
|
|
981
1069
|
}
|
|
982
1070
|
const isMirrorOnly = NON_GATED_MIRROR_TOOLS.has(input.tool_name);
|
|
983
|
-
const decisionSource = isMirrorOnly ? void 0 : deriveDecisionSource(lookup.tool, lookup.input, liveMode, input.cwd);
|
|
984
|
-
const beacon = !isMirrorOnly && decisionSource === "policy_auto" && throttlePass(`autodecision:${sessionKey}:${lookup.tool}`, AUTO_DECISION_WINDOW_MS) ? deriveAutoDecisionBeacon(lookup.tool, lookup.input, liveMode, input.cwd) : void 0;
|
|
1071
|
+
const decisionSource = isMirrorOnly ? void 0 : deriveDecisionSource(lookup.tool, lookup.input, liveMode, agent.type, input.cwd);
|
|
1072
|
+
const beacon = !isMirrorOnly && decisionSource === "policy_auto" && throttlePass(`autodecision:${sessionKey}:${lookup.tool}`, AUTO_DECISION_WINDOW_MS) ? deriveAutoDecisionBeacon(lookup.tool, lookup.input, liveMode, agent.type, input.cwd) : void 0;
|
|
985
1073
|
const toolReport = reportEvent({
|
|
986
1074
|
event: isError ? "tool_error" : "tool_complete",
|
|
987
1075
|
agentType: agent.type,
|
|
@@ -1235,7 +1323,12 @@ var handleStopFailure = async (input, agent = CLAUDE_CODE_AGENT) => {
|
|
|
1235
1323
|
|
|
1236
1324
|
export {
|
|
1237
1325
|
getPolicy,
|
|
1326
|
+
isAutoApprove,
|
|
1327
|
+
isAutoDeny,
|
|
1328
|
+
resolveAutoResolveOrigin,
|
|
1238
1329
|
resolvePolicy,
|
|
1330
|
+
resolvePolicyAcross,
|
|
1331
|
+
isModeStateDegraded,
|
|
1239
1332
|
fetchModeState,
|
|
1240
1333
|
fetchModeOverride,
|
|
1241
1334
|
askUser,
|
|
@@ -1260,6 +1353,7 @@ export {
|
|
|
1260
1353
|
toCodexWire,
|
|
1261
1354
|
describeApplyPatch,
|
|
1262
1355
|
toPolicyLookup,
|
|
1356
|
+
toPolicyLookups,
|
|
1263
1357
|
permissionTimeoutDecision,
|
|
1264
1358
|
preToolUseTimeoutDecision,
|
|
1265
1359
|
readLastPrompt,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isAutoApprove,
|
|
3
|
+
isAutoDeny,
|
|
4
|
+
isModeStateDegraded,
|
|
5
|
+
resolveAutoResolveOrigin,
|
|
6
|
+
resolvePolicyAcross
|
|
7
|
+
} from "./chunk-FZIQSDTQ.js";
|
|
8
|
+
import {
|
|
9
|
+
evaluateScope,
|
|
10
|
+
scopeChangeReason,
|
|
11
|
+
scopeRequiresHuman
|
|
12
|
+
} from "./chunk-DQAN3JQP.js";
|
|
13
|
+
|
|
14
|
+
// src/answer.ts
|
|
15
|
+
var denyReasonFrom = (value, note) => {
|
|
16
|
+
const trimmed = note?.trim();
|
|
17
|
+
if (trimmed) return `Denied from your phone: ${trimmed}`;
|
|
18
|
+
return value && value !== "no" && value !== "yes" ? `Denied from your phone: ${value}` : "Denied via push notification";
|
|
19
|
+
};
|
|
20
|
+
var isDeferAnswer = (value) => value === "defer";
|
|
21
|
+
|
|
22
|
+
// src/gate.ts
|
|
23
|
+
var KILL_REASON = "Stopped by user, this agent was halted from Pushary";
|
|
24
|
+
var strictestScopeVerdict = (scope, paths) => {
|
|
25
|
+
if (paths.length === 0) return evaluateScope(scope, void 0);
|
|
26
|
+
let worst = { outcome: "in_scope" };
|
|
27
|
+
for (const path of paths) {
|
|
28
|
+
const verdict = evaluateScope(scope, path);
|
|
29
|
+
if (verdict.outcome === "off_limits") return verdict;
|
|
30
|
+
if (verdict.outcome === "out_of_scope") worst = verdict;
|
|
31
|
+
}
|
|
32
|
+
return worst;
|
|
33
|
+
};
|
|
34
|
+
var survivesDegraded = (config, toolName, toolInputs, cwd, repoKey) => toolInputs.length === 1 && resolveAutoResolveOrigin(config, toolName, toolInputs[0], cwd, repoKey) === "safe_readonly";
|
|
35
|
+
var resolveGate = (input) => {
|
|
36
|
+
const { modeState, config, toolName, toolInputs, cwd, repoKey } = input;
|
|
37
|
+
if (modeState.kill) return { kind: "kill", reason: KILL_REASON };
|
|
38
|
+
const policy = resolvePolicyAcross(config, toolName, modeState.mode, toolInputs, cwd, repoKey);
|
|
39
|
+
const autoApproves = isAutoApprove(policy);
|
|
40
|
+
const autoDenies = isAutoDeny(policy);
|
|
41
|
+
if (autoDenies) return { kind: "deny", policy, reason: `Denied by policy for ${policy.tool}` };
|
|
42
|
+
if (isModeStateDegraded(modeState)) {
|
|
43
|
+
if (autoApproves) {
|
|
44
|
+
return survivesDegraded(config, toolName, toolInputs, cwd, repoKey) ? { kind: "allow", policy } : { kind: "defer", policy };
|
|
45
|
+
}
|
|
46
|
+
return { kind: "gate", policy };
|
|
47
|
+
}
|
|
48
|
+
const scopeVerdict = modeState.scope ? strictestScopeVerdict(modeState.scope, input.scopePaths ?? []) : { outcome: "no_contract" };
|
|
49
|
+
if (autoApproves) {
|
|
50
|
+
if (!scopeRequiresHuman(scopeVerdict)) return { kind: "allow", policy };
|
|
51
|
+
}
|
|
52
|
+
const scopeReason = scopeChangeReason(scopeVerdict);
|
|
53
|
+
return {
|
|
54
|
+
kind: "gate",
|
|
55
|
+
policy,
|
|
56
|
+
scopeReason,
|
|
57
|
+
scopePath: scopeReason && "path" in scopeVerdict ? scopeVerdict.path : void 0
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export {
|
|
62
|
+
denyReasonFrom,
|
|
63
|
+
isDeferAnswer,
|
|
64
|
+
KILL_REASON,
|
|
65
|
+
resolveGate
|
|
66
|
+
};
|