@pushary/agent-hooks 0.87.2 → 0.87.4
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/data/cursor-plugin/hooks/hooks.json +7 -0
- package/data/cursor-plugin/scripts/pushary-gate.mjs +94 -28
- package/data/cursor-plugin/scripts/pushary-gate.test.mjs +81 -1
- package/dist/bin/pushary-bell-hook.js +4 -4
- package/dist/bin/pushary-bell.js +7 -6
- package/dist/bin/pushary-claude.js +14 -11
- package/dist/bin/pushary-clean.js +36 -20
- package/dist/bin/pushary-codex-bridge.js +6 -5
- package/dist/bin/pushary-codex-hook.js +129 -52
- package/dist/bin/pushary-codex.js +4 -3
- package/dist/bin/pushary-connect.js +6 -6
- package/dist/bin/pushary-daemon.js +5 -4
- package/dist/bin/pushary-disconnect.js +65 -8
- package/dist/bin/pushary-doctor.js +79 -18
- package/dist/bin/pushary-elicitation-hook.js +1 -1
- package/dist/bin/pushary-gemini-bridge.js +6 -5
- package/dist/bin/pushary-gemini-hook.js +12 -10
- package/dist/bin/pushary-hook.js +6 -5
- package/dist/bin/pushary-login.js +4 -4
- package/dist/bin/pushary-logout.js +5 -4
- package/dist/bin/pushary-mode.js +3 -3
- package/dist/bin/pushary-notification-hook.js +4 -3
- package/dist/bin/pushary-opencode-hook.d.ts +1 -0
- package/dist/bin/pushary-opencode-hook.js +329 -0
- package/dist/bin/pushary-permission-denied-hook.js +6 -5
- package/dist/bin/pushary-permission-hook.js +6 -5
- package/dist/bin/pushary-post-hook.js +4 -3
- package/dist/bin/pushary-prompt-hook.js +4 -3
- package/dist/bin/pushary-session-end-hook.js +4 -3
- package/dist/bin/pushary-session-start-hook.js +4 -3
- package/dist/bin/pushary-setup.js +238 -84
- package/dist/bin/pushary-stats.js +2 -2
- package/dist/bin/pushary-status.js +5 -5
- package/dist/bin/pushary-stop-hook.js +4 -3
- package/dist/bin/pushary-stopfailure-hook.js +4 -3
- package/dist/bin/pushary-upgrade.js +7 -6
- package/dist/bin/pushary-wait.js +3 -3
- package/dist/{chunk-5646VKR4.js → chunk-2JYQAW4B.js} +5 -53
- package/dist/chunk-7J67NYK4.js +287 -0
- package/dist/{chunk-M7RYIZX6.js → chunk-B7ZBHWOW.js} +10 -1
- package/dist/{chunk-QBWYNVKY.js → chunk-BFBAHOGR.js} +1 -1
- package/dist/{chunk-7YJS2VTG.js → chunk-DMW54VWK.js} +12 -1
- package/dist/{chunk-3LKYBFWA.js → chunk-EJL3PJU7.js} +1 -1
- package/dist/chunk-GIRT7677.js +11 -0
- package/dist/{chunk-SJVLFIYP.js → chunk-H5PIPIKQ.js} +1 -1
- package/dist/{chunk-HAUTLCMA.js → chunk-ICERLAE2.js} +1 -1
- package/dist/{chunk-ZZE6FN7T.js → chunk-K42RXJPG.js} +52 -1
- package/dist/{chunk-Y3UTTFTZ.js → chunk-KBS53WOE.js} +1 -1
- package/dist/{chunk-A4I5JGCW.js → chunk-KFAH6KRE.js} +1 -1
- package/dist/{chunk-LT2VLPUK.js → chunk-KULUNLRO.js} +18 -2
- package/dist/{chunk-B3V2H6SA.js → chunk-NAPRGHZ3.js} +54 -13
- package/dist/{chunk-HJ44HIUW.js → chunk-OMA2OZRE.js} +11 -9
- package/dist/{chunk-ECEUMWBH.js → chunk-QLMX5LLV.js} +1 -1
- package/dist/{chunk-XW5CHAQX.js → chunk-QXZ2CKRK.js} +2 -2
- package/dist/{chunk-JAICP7BA.js → chunk-TD3M232T.js} +1 -1
- package/dist/{chunk-WLLBFDVY.js → chunk-TV3UFGT7.js} +1 -1
- package/dist/{chunk-7MBDFNUC.js → chunk-VIHRP25S.js} +1 -1
- package/dist/{chunk-V2XPF77V.js → chunk-VOE4SYPR.js} +42 -279
- package/dist/{chunk-OT4YD54Q.js → chunk-YX6ZKCC5.js} +1 -1
- package/dist/{chunk-2T3GX7WG.js → chunk-YYTLTEGU.js} +1 -1
- package/dist/chunk-ZGC3W23N.js +371 -0
- package/dist/src/index.js +6 -5
- package/package.json +2 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Pushary gate — Cursor `beforeShellExecution`
|
|
2
|
+
// Pushary gate — Cursor `beforeShellExecution` and `beforeMCPExecution` hooks.
|
|
3
3
|
//
|
|
4
4
|
// Routes risky shell commands through your Pushary permission policy before they
|
|
5
5
|
// run. Which commands reach this gate is the `matcher` in ../hooks/hooks.json; what
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
// Self-contained: no dependencies, uses the global fetch (Node 18+).
|
|
17
17
|
//
|
|
18
18
|
// Contract (https://cursor.com/docs/hooks):
|
|
19
|
-
// stdin : { "
|
|
19
|
+
// stdin : { "hook_event_name": string, "cwd": string, "conversation_id": string, ... }
|
|
20
|
+
// beforeShellExecution adds { "command": string }
|
|
21
|
+
// beforeMCPExecution adds { "tool_name", "tool_input", "mcp_server_name" }
|
|
20
22
|
// stdout : { "permission": "allow" | "deny" | "ask", "user_message"?, "agent_message"? }
|
|
21
23
|
//
|
|
22
24
|
// Failure model: every handled path writes a decision and exits 0. Network/parse
|
|
@@ -245,12 +247,12 @@ const callTool = async (apiKey, name, args) => {
|
|
|
245
247
|
const GATE_TIMEOUT_MS = 5000
|
|
246
248
|
|
|
247
249
|
/**
|
|
248
|
-
* The verdict for one
|
|
250
|
+
* The verdict for one request, or null if we could not get one.
|
|
249
251
|
*
|
|
250
252
|
* Null is not a denial and not an approval: every failure here hands the call
|
|
251
253
|
* back to Cursor's own prompt, exactly as if this gate were not installed.
|
|
252
254
|
*/
|
|
253
|
-
const decide = async (apiKey,
|
|
255
|
+
const decide = async (apiKey, request, cwd, sessionId) => {
|
|
254
256
|
try {
|
|
255
257
|
const response = await fetch(`${BASE_URL}/api/agent/gate`, {
|
|
256
258
|
method: 'POST',
|
|
@@ -258,8 +260,8 @@ const decide = async (apiKey, command, cwd, sessionId) => {
|
|
|
258
260
|
body: JSON.stringify({
|
|
259
261
|
v: 1,
|
|
260
262
|
source: 'cursor',
|
|
261
|
-
toolName:
|
|
262
|
-
toolInputs:
|
|
263
|
+
toolName: request.toolName,
|
|
264
|
+
toolInputs: request.toolInputs,
|
|
263
265
|
cwd,
|
|
264
266
|
repoKey: deriveRepoKey(cwd),
|
|
265
267
|
sessionId,
|
|
@@ -341,16 +343,16 @@ const deriveActionBody = (command) => capActionBody(redactSecretsDeep(command))
|
|
|
341
343
|
// lock screen ungated.
|
|
342
344
|
const commandHead = (command) => command.trim().split(/\s+/).slice(0, 2).join(' ').slice(0, 120)
|
|
343
345
|
|
|
344
|
-
const askArgs = (
|
|
345
|
-
question:
|
|
346
|
+
const askArgs = (request, project, ident) => ({
|
|
347
|
+
question: `${request.prompt}\n\n${redactSecrets(request.display)}`,
|
|
346
348
|
type: 'confirm',
|
|
347
|
-
context: `Cursor agent wants to
|
|
349
|
+
context: `Cursor agent wants to ${request.verb} in ${project}`,
|
|
348
350
|
agentName: ident.agentName,
|
|
349
351
|
sessionId: ident.sessionId,
|
|
350
352
|
machineId: ident.machineId,
|
|
351
|
-
toolName:
|
|
352
|
-
toolTarget:
|
|
353
|
-
actionBody: deriveActionBody(
|
|
353
|
+
toolName: request.toolName,
|
|
354
|
+
toolTarget: request.toolTarget,
|
|
355
|
+
actionBody: deriveActionBody(request.display),
|
|
354
356
|
wait: false,
|
|
355
357
|
})
|
|
356
358
|
|
|
@@ -374,13 +376,77 @@ const pollForAnswer = async (apiKey, correlationId, deadlineMs) => {
|
|
|
374
376
|
const fromTimeoutAction = (action, deniedReason) =>
|
|
375
377
|
action === 'approve' ? ALLOW : action === 'deny' ? deny(deniedReason) : ask()
|
|
376
378
|
|
|
377
|
-
const
|
|
379
|
+
const denialFor = (noun) =>
|
|
380
|
+
`The user denied this ${noun} via a Pushary push approval. Do not run it — propose an alternative or ask how to proceed.`
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* What this hook is being asked about, in the one shape the rest of the gate
|
|
384
|
+
* reads.
|
|
385
|
+
*
|
|
386
|
+
* Cursor sends a different payload per event and the gate used to know only one
|
|
387
|
+
* of them: it read `input.command`, called everything "Bash", and any event that
|
|
388
|
+
* was not a shell execution fell out at the empty-command guard as `ask`. That
|
|
389
|
+
* was correct while `beforeShellExecution` was the only thing registered.
|
|
390
|
+
*
|
|
391
|
+
* Returns null when there is nothing to gate, which the caller answers with
|
|
392
|
+
* Cursor's own prompt.
|
|
393
|
+
*/
|
|
394
|
+
const describeRequest = (input) => {
|
|
395
|
+
const event = typeof input.hook_event_name === 'string' ? input.hook_event_name : ''
|
|
396
|
+
|
|
397
|
+
if (event === 'beforeMCPExecution') {
|
|
398
|
+
const tool = typeof input.tool_name === 'string' ? input.tool_name.trim() : ''
|
|
399
|
+
if (!tool) return null
|
|
400
|
+
const server = typeof input.mcp_server_name === 'string' ? input.mcp_server_name.trim() : ''
|
|
401
|
+
// `mcp__<server>__<tool>`, the same spelling Claude Code uses and the one
|
|
402
|
+
// `validateToolPattern` accepts, so a policy someone wrote once matches the
|
|
403
|
+
// same tool whichever agent called it. That portability is the only reason
|
|
404
|
+
// the policy engine is server-side.
|
|
405
|
+
const toolName = server ? `mcp__${server}__${tool}` : `mcp__${tool}`
|
|
406
|
+
|
|
407
|
+
// Never gate Pushary's own MCP tools. Asking Pushary to approve Pushary's
|
|
408
|
+
// ask_user call deadlocks: the approval cannot be delivered until the call
|
|
409
|
+
// it is gating goes through. The Claude hook carries the same guard, where
|
|
410
|
+
// it is a precaution because the matcher does not route these there. Here it
|
|
411
|
+
// is load-bearing: `beforeMCPExecution` is registered with NO matcher, so
|
|
412
|
+
// every MCP call reaches this gate, ours included.
|
|
413
|
+
if (toolName.startsWith('mcp__pushary__')) return null
|
|
414
|
+
|
|
415
|
+
const params = typeof input.tool_input === 'string'
|
|
416
|
+
? input.tool_input
|
|
417
|
+
: input.tool_input === undefined ? '' : JSON.stringify(input.tool_input)
|
|
418
|
+
return {
|
|
419
|
+
toolName,
|
|
420
|
+
toolInputs: [{ tool: toolName, params }],
|
|
421
|
+
display: params ? `${toolName}\n${params}` : toolName,
|
|
422
|
+
toolTarget: toolName.slice(0, 120),
|
|
423
|
+
prompt: 'Allow this tool call?',
|
|
424
|
+
verb: 'call this tool',
|
|
425
|
+
denied: denialFor('tool call'),
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Shell is also the fallback for a payload with no event name: a `command` is
|
|
430
|
+
// what `beforeShellExecution` has always been recognised by, and an older
|
|
431
|
+
// Cursor that omits `hook_event_name` must keep working exactly as before.
|
|
432
|
+
const command = typeof input.command === 'string' ? input.command.trim() : ''
|
|
433
|
+
if (!command || (event && event !== 'beforeShellExecution')) return null
|
|
434
|
+
return {
|
|
435
|
+
toolName: 'Bash',
|
|
436
|
+
toolInputs: [{ command }],
|
|
437
|
+
display: command,
|
|
438
|
+
toolTarget: commandHead(command),
|
|
439
|
+
prompt: 'Allow this command?',
|
|
440
|
+
verb: 'run this',
|
|
441
|
+
denied: denialFor('command'),
|
|
442
|
+
}
|
|
443
|
+
}
|
|
378
444
|
|
|
379
445
|
// push_only: wait up to the policy timeout, then apply the timeout action.
|
|
380
|
-
const handlePushOnly = async (apiKey,
|
|
446
|
+
const handlePushOnly = async (apiKey, request, project, ident, timeoutSeconds, timeoutAction) => {
|
|
381
447
|
let asked
|
|
382
448
|
try {
|
|
383
|
-
asked = await withRetry(() => callTool(apiKey, 'ask_user', askArgs(
|
|
449
|
+
asked = await withRetry(() => callTool(apiKey, 'ask_user', askArgs(request, project, ident)), 3)
|
|
384
450
|
} catch {
|
|
385
451
|
return fromTimeoutAction(timeoutAction, 'Push notification failed; denied per your Pushary policy.')
|
|
386
452
|
}
|
|
@@ -389,7 +455,7 @@ const handlePushOnly = async (apiKey, command, project, ident, timeoutSeconds, t
|
|
|
389
455
|
const realMs = Math.max(timeoutSeconds, 1) * 1000
|
|
390
456
|
const cap = Math.min(realMs, MAX_BLOCK_MS)
|
|
391
457
|
const answer = await pollForAnswer(apiKey, asked.correlationId, Date.now() + cap)
|
|
392
|
-
if (answer.answered) return answer.value === 'yes' ? ALLOW : deny(
|
|
458
|
+
if (answer.answered) return answer.value === 'yes' ? ALLOW : deny(request.denied)
|
|
393
459
|
|
|
394
460
|
// If Cursor's hook limit cut us off before the configured timeout, hand off to
|
|
395
461
|
// Cursor's own prompt rather than misapplying the policy's timeout action.
|
|
@@ -398,10 +464,10 @@ const handlePushOnly = async (apiKey, command, project, ident, timeoutSeconds, t
|
|
|
398
464
|
}
|
|
399
465
|
|
|
400
466
|
// push_first: race the push for a short window, then fall back to Cursor's prompt.
|
|
401
|
-
const handlePushFirst = async (apiKey,
|
|
467
|
+
const handlePushFirst = async (apiKey, request, project, ident, pushFirstSeconds) => {
|
|
402
468
|
let asked
|
|
403
469
|
try {
|
|
404
|
-
asked = await withRetry(() => callTool(apiKey, 'ask_user', askArgs(
|
|
470
|
+
asked = await withRetry(() => callTool(apiKey, 'ask_user', askArgs(request, project, ident)), 3)
|
|
405
471
|
} catch {
|
|
406
472
|
return ask()
|
|
407
473
|
}
|
|
@@ -409,16 +475,16 @@ const handlePushFirst = async (apiKey, command, project, ident, pushFirstSeconds
|
|
|
409
475
|
|
|
410
476
|
const cap = Math.min(Math.max(pushFirstSeconds, 1) * 1000, MAX_BLOCK_MS)
|
|
411
477
|
const answer = await pollForAnswer(apiKey, asked.correlationId, Date.now() + cap)
|
|
412
|
-
if (answer.answered) return answer.value === 'yes' ? ALLOW : deny(
|
|
478
|
+
if (answer.answered) return answer.value === 'yes' ? ALLOW : deny(request.denied)
|
|
413
479
|
return ask('Sent to your phone via Pushary — you can also approve here.')
|
|
414
480
|
}
|
|
415
481
|
|
|
416
482
|
// notify_only: fire an awareness notification, let Cursor's prompt decide.
|
|
417
|
-
const handleNotifyOnly = async (apiKey,
|
|
483
|
+
const handleNotifyOnly = async (apiKey, request, project, ident) => {
|
|
418
484
|
try {
|
|
419
485
|
await callTool(apiKey, 'send_notification', {
|
|
420
486
|
title: 'Agent needs approval',
|
|
421
|
-
body: redactSecrets(
|
|
487
|
+
body: redactSecrets(request.display).slice(0, 180),
|
|
422
488
|
agentName: ident.agentName,
|
|
423
489
|
sessionId: ident.sessionId,
|
|
424
490
|
machineId: ident.machineId,
|
|
@@ -436,8 +502,8 @@ const main = async () => {
|
|
|
436
502
|
return respond(ask())
|
|
437
503
|
}
|
|
438
504
|
|
|
439
|
-
const
|
|
440
|
-
if (!
|
|
505
|
+
const request = describeRequest(input)
|
|
506
|
+
if (!request) return respond(ask())
|
|
441
507
|
|
|
442
508
|
const apiKey = resolveApiKey()
|
|
443
509
|
if (!apiKey) {
|
|
@@ -451,7 +517,7 @@ const main = async () => {
|
|
|
451
517
|
const ident = { agentName: `Cursor - ${project}`, sessionId, machineId: getMachineId() }
|
|
452
518
|
|
|
453
519
|
try {
|
|
454
|
-
const verdict = await decide(apiKey,
|
|
520
|
+
const verdict = await decide(apiKey, request, input.cwd, sessionId)
|
|
455
521
|
|
|
456
522
|
// No verdict, or one that says nothing: Cursor's own prompt decides, exactly
|
|
457
523
|
// as if this gate were not installed. Never a forced denial on an outage.
|
|
@@ -467,12 +533,12 @@ const main = async () => {
|
|
|
467
533
|
case 'terminal_only':
|
|
468
534
|
return respond(ask())
|
|
469
535
|
case 'notify_only':
|
|
470
|
-
return respond(await handleNotifyOnly(apiKey,
|
|
536
|
+
return respond(await handleNotifyOnly(apiKey, request, project, ident))
|
|
471
537
|
case 'push_only':
|
|
472
|
-
return respond(await handlePushOnly(apiKey,
|
|
538
|
+
return respond(await handlePushOnly(apiKey, request, project, ident, tool.timeoutSeconds, tool.timeoutAction))
|
|
473
539
|
case 'push_first':
|
|
474
540
|
default:
|
|
475
|
-
return respond(await handlePushFirst(apiKey,
|
|
541
|
+
return respond(await handlePushFirst(apiKey, request, project, ident, tool.pushFirstSeconds))
|
|
476
542
|
}
|
|
477
543
|
} catch (error) {
|
|
478
544
|
process.stderr.write(`[pushary-gate] ${error?.message ?? error}\n`)
|
|
@@ -482,7 +548,7 @@ const main = async () => {
|
|
|
482
548
|
|
|
483
549
|
// Exported for scripts/pushary-gate.test.mjs. main() only runs when the gate is
|
|
484
550
|
// executed directly, so importing this file for a test does not read stdin.
|
|
485
|
-
export { normalizeRepoRemote, deriveRepoKey }
|
|
551
|
+
export { normalizeRepoRemote, deriveRepoKey, describeRequest }
|
|
486
552
|
|
|
487
553
|
const isDirectRun = process.argv[1] && import.meta.url === `file://${process.argv[1]}`
|
|
488
554
|
if (!isDirectRun) {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import assert from 'node:assert/strict'
|
|
16
16
|
import { describe, it } from 'node:test'
|
|
17
|
-
import { normalizeRepoRemote } from './pushary-gate.mjs'
|
|
17
|
+
import { describeRequest, normalizeRepoRemote } from './pushary-gate.mjs'
|
|
18
18
|
|
|
19
19
|
describe('repo identity', () => {
|
|
20
20
|
it('collapses the forms of one remote', () => {
|
|
@@ -33,3 +33,83 @@ describe('repo identity', () => {
|
|
|
33
33
|
for (const r of ['', '../rel', 'file:///Users/dev/repo']) assert.equal(normalizeRepoRemote(r), undefined, r)
|
|
34
34
|
})
|
|
35
35
|
})
|
|
36
|
+
|
|
37
|
+
// The gate answers two Cursor events now, and they arrive on the same stdin with
|
|
38
|
+
// different fields. What each one turns into is the only thing standing between
|
|
39
|
+
// an MCP call and being gated as if it were a shell command.
|
|
40
|
+
describe('what the gate was asked about', () => {
|
|
41
|
+
it('names an MCP call the way every other agent names one', () => {
|
|
42
|
+
const request = describeRequest({
|
|
43
|
+
hook_event_name: 'beforeMCPExecution',
|
|
44
|
+
tool_name: 'create_issue',
|
|
45
|
+
mcp_server_name: 'github',
|
|
46
|
+
tool_input: '{"title":"x"}',
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// `mcp__<server>__<tool>` is what `validateToolPattern` accepts and what
|
|
50
|
+
// Claude Code sends, so one policy rule covers the tool on both agents. A
|
|
51
|
+
// Cursor-only spelling would silently miss every rule a user already wrote.
|
|
52
|
+
assert.equal(request.toolName, 'mcp__github__create_issue')
|
|
53
|
+
assert.deepEqual(request.toolInputs, [{ tool: 'mcp__github__create_issue', params: '{"title":"x"}' }])
|
|
54
|
+
assert.match(request.display, /create_issue/)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('still reads a shell command, including from a payload with no event name', () => {
|
|
58
|
+
const named = describeRequest({ hook_event_name: 'beforeShellExecution', command: ' rm -rf /tmp/x ' })
|
|
59
|
+
assert.equal(named.toolName, 'Bash')
|
|
60
|
+
assert.equal(named.toolTarget, 'rm -rf')
|
|
61
|
+
|
|
62
|
+
// An older Cursor that omits `hook_event_name` must keep working exactly as
|
|
63
|
+
// it did when shell was the only registered event.
|
|
64
|
+
assert.equal(describeRequest({ command: 'git push' }).toolName, 'Bash')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* `beforeMCPExecution` is registered with NO matcher, so every MCP call in the
|
|
69
|
+
* editor reaches this gate, ours included. Gating our own tools deadlocks: the
|
|
70
|
+
* approval for `ask_user` cannot reach the phone until the `ask_user` call it
|
|
71
|
+
* is gating goes through. With `failClosed: true` on that entry it is worse
|
|
72
|
+
* than a hang, because a timeout turns into a denial of the user's own
|
|
73
|
+
* question.
|
|
74
|
+
*
|
|
75
|
+
* The Claude hook carries this guard three times over and calls it a
|
|
76
|
+
* precaution, because its matcher does not route these there. Here it is the
|
|
77
|
+
* only thing standing between the two.
|
|
78
|
+
*/
|
|
79
|
+
it('never gates Pushary\'s own MCP tools', () => {
|
|
80
|
+
for (const tool of ['ask_user', 'wait_for_answer', 'send_notification', 'propose_scope']) {
|
|
81
|
+
assert.equal(
|
|
82
|
+
describeRequest({
|
|
83
|
+
hook_event_name: 'beforeMCPExecution',
|
|
84
|
+
tool_name: tool,
|
|
85
|
+
mcp_server_name: 'pushary',
|
|
86
|
+
tool_input: '{}',
|
|
87
|
+
}),
|
|
88
|
+
null,
|
|
89
|
+
`mcp__pushary__${tool} must reach Cursor's own prompt, not ours`,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// A neighbour whose name merely starts the same way is still gated. The
|
|
94
|
+
// guard is on the `mcp__pushary__` prefix, not on "contains pushary".
|
|
95
|
+
assert.equal(
|
|
96
|
+
describeRequest({
|
|
97
|
+
hook_event_name: 'beforeMCPExecution',
|
|
98
|
+
tool_name: 'run',
|
|
99
|
+
mcp_server_name: 'pushary-clone',
|
|
100
|
+
tool_input: '{}',
|
|
101
|
+
}).toolName,
|
|
102
|
+
'mcp__pushary-clone__run',
|
|
103
|
+
)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('declines an event it does not gate rather than calling it Bash', () => {
|
|
107
|
+
// beforeReadFile is registered by nobody here on purpose: its response
|
|
108
|
+
// schema has no `ask`, so the no-opinion path this gate takes on every error
|
|
109
|
+
// has nothing to return. If one ever arrives, it must not be mistaken for a
|
|
110
|
+
// shell command carrying no command.
|
|
111
|
+
assert.equal(describeRequest({ hook_event_name: 'beforeReadFile', file_path: '/etc/passwd' }), null)
|
|
112
|
+
assert.equal(describeRequest({ hook_event_name: 'beforeMCPExecution' }), null)
|
|
113
|
+
assert.equal(describeRequest({}), null)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
handleBell
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-YYTLTEGU.js";
|
|
5
5
|
import {
|
|
6
6
|
readHookInput
|
|
7
7
|
} from "../chunk-6CUUA7HO.js";
|
|
8
|
-
import "../chunk-
|
|
8
|
+
import "../chunk-YX6ZKCC5.js";
|
|
9
9
|
import "../chunk-BC3VCZ3E.js";
|
|
10
10
|
import "../chunk-6MTNS63X.js";
|
|
11
|
-
import "../chunk-
|
|
11
|
+
import "../chunk-KBS53WOE.js";
|
|
12
12
|
import "../chunk-2UMNXADU.js";
|
|
13
|
-
import "../chunk-
|
|
13
|
+
import "../chunk-DMW54VWK.js";
|
|
14
14
|
|
|
15
15
|
// bin/pushary-bell-hook.ts
|
|
16
16
|
var main = async () => {
|
package/dist/bin/pushary-bell.js
CHANGED
|
@@ -5,18 +5,19 @@ import {
|
|
|
5
5
|
liveAgentCount,
|
|
6
6
|
ring,
|
|
7
7
|
upgradeThreshold
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-YYTLTEGU.js";
|
|
9
9
|
import {
|
|
10
10
|
guardBinary
|
|
11
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-B7ZBHWOW.js";
|
|
12
12
|
import "../chunk-VT4IVERX.js";
|
|
13
13
|
import {
|
|
14
14
|
claudeSettings
|
|
15
|
-
} from "../chunk-
|
|
15
|
+
} from "../chunk-NAPRGHZ3.js";
|
|
16
|
+
import "../chunk-GIRT7677.js";
|
|
16
17
|
import {
|
|
17
18
|
createIo
|
|
18
19
|
} from "../chunk-5RUIFDTP.js";
|
|
19
|
-
import "../chunk-
|
|
20
|
+
import "../chunk-YX6ZKCC5.js";
|
|
20
21
|
import {
|
|
21
22
|
parseFlags
|
|
22
23
|
} from "../chunk-GMXKITVA.js";
|
|
@@ -30,13 +31,13 @@ import {
|
|
|
30
31
|
readJsonSafe,
|
|
31
32
|
writeJsonAtomic
|
|
32
33
|
} from "../chunk-6MTNS63X.js";
|
|
33
|
-
import "../chunk-
|
|
34
|
+
import "../chunk-KBS53WOE.js";
|
|
34
35
|
import "../chunk-7QLSKOSU.js";
|
|
35
36
|
import {
|
|
36
37
|
EXIT
|
|
37
38
|
} from "../chunk-KZERVKTD.js";
|
|
38
39
|
import "../chunk-2UMNXADU.js";
|
|
39
|
-
import "../chunk-
|
|
40
|
+
import "../chunk-DMW54VWK.js";
|
|
40
41
|
|
|
41
42
|
// src/bell/install.ts
|
|
42
43
|
import { join } from "path";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ensureDaemonRunning
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-H5PIPIKQ.js";
|
|
5
5
|
import {
|
|
6
6
|
buildTranscriptRecords,
|
|
7
7
|
decodePublicKey,
|
|
@@ -9,34 +9,37 @@ import {
|
|
|
9
9
|
generateSessionKey,
|
|
10
10
|
uploadTranscriptRecords,
|
|
11
11
|
wrapSessionKey
|
|
12
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-QLMX5LLV.js";
|
|
13
13
|
import {
|
|
14
14
|
startProcessOwnership,
|
|
15
15
|
withSpawnProcessOwnership
|
|
16
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-BFBAHOGR.js";
|
|
17
17
|
import {
|
|
18
18
|
needsShell,
|
|
19
19
|
spawnClaude
|
|
20
20
|
} from "../chunk-XHKBHWLX.js";
|
|
21
21
|
import "../chunk-VT4IVERX.js";
|
|
22
|
-
import "../chunk-
|
|
22
|
+
import "../chunk-NAPRGHZ3.js";
|
|
23
23
|
import {
|
|
24
24
|
isDeferAnswer
|
|
25
25
|
} from "../chunk-YHG74UFF.js";
|
|
26
|
+
import "../chunk-GIRT7677.js";
|
|
26
27
|
import {
|
|
27
|
-
deriveToolTarget,
|
|
28
|
-
describeToolCall,
|
|
29
28
|
fetchModeState,
|
|
30
29
|
getPolicy,
|
|
31
30
|
repoKeyFor,
|
|
32
|
-
reportEvent
|
|
33
|
-
|
|
34
|
-
} from "../chunk-V2XPF77V.js";
|
|
31
|
+
reportEvent
|
|
32
|
+
} from "../chunk-VOE4SYPR.js";
|
|
35
33
|
import {
|
|
36
34
|
askUser,
|
|
37
35
|
cancelQuestion,
|
|
38
36
|
waitForAnswer
|
|
39
37
|
} from "../chunk-J3YDT4HM.js";
|
|
38
|
+
import {
|
|
39
|
+
deriveToolTarget,
|
|
40
|
+
describeToolCall,
|
|
41
|
+
scopePathFor
|
|
42
|
+
} from "../chunk-7J67NYK4.js";
|
|
40
43
|
import {
|
|
41
44
|
getMachineId
|
|
42
45
|
} from "../chunk-RN3NOEJF.js";
|
|
@@ -44,7 +47,7 @@ import "../chunk-DINDL2CF.js";
|
|
|
44
47
|
import "../chunk-6MTNS63X.js";
|
|
45
48
|
import "../chunk-BSZYIAZL.js";
|
|
46
49
|
import "../chunk-SAF6HGAA.js";
|
|
47
|
-
import "../chunk-
|
|
50
|
+
import "../chunk-KBS53WOE.js";
|
|
48
51
|
import "../chunk-7QLSKOSU.js";
|
|
49
52
|
import "../chunk-KZERVKTD.js";
|
|
50
53
|
import {
|
|
@@ -54,7 +57,7 @@ import {
|
|
|
54
57
|
import {
|
|
55
58
|
KILL_REASON,
|
|
56
59
|
resolveGate
|
|
57
|
-
} from "../chunk-
|
|
60
|
+
} from "../chunk-DMW54VWK.js";
|
|
58
61
|
|
|
59
62
|
// src/wrapper/claudeBinary.ts
|
|
60
63
|
import { statSync } from "fs";
|
|
@@ -2,18 +2,20 @@
|
|
|
2
2
|
import {
|
|
3
3
|
removeClaudeMcpServers,
|
|
4
4
|
removePusharySettings
|
|
5
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-2JYQAW4B.js";
|
|
6
6
|
import {
|
|
7
|
+
CURSOR_GATE_EVENTS,
|
|
7
8
|
removeInstructionBlock,
|
|
8
9
|
shortenHome,
|
|
9
10
|
unregisterPluginLocation,
|
|
10
11
|
vscodeSettingsTargets
|
|
11
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-KULUNLRO.js";
|
|
13
|
+
import "../chunk-ZGC3W23N.js";
|
|
12
14
|
import {
|
|
13
15
|
removeGeminiSettings
|
|
14
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-K42RXJPG.js";
|
|
15
17
|
import "../chunk-ATBKJNWS.js";
|
|
16
|
-
import "../chunk-
|
|
18
|
+
import "../chunk-B7ZBHWOW.js";
|
|
17
19
|
import {
|
|
18
20
|
execNpm
|
|
19
21
|
} from "../chunk-VT4IVERX.js";
|
|
@@ -24,6 +26,7 @@ import {
|
|
|
24
26
|
claudeSkillDir,
|
|
25
27
|
codexAgentsMd,
|
|
26
28
|
codexConfigToml,
|
|
29
|
+
codexHookPositions,
|
|
27
30
|
codexHooksJson,
|
|
28
31
|
codexSkillDir,
|
|
29
32
|
cursorPluginDir,
|
|
@@ -32,9 +35,12 @@ import {
|
|
|
32
35
|
geminiMd,
|
|
33
36
|
geminiSettings,
|
|
34
37
|
pusharyDir,
|
|
38
|
+
removeCodexHookTrust,
|
|
35
39
|
removeCodexHooks,
|
|
36
40
|
vscodePluginDir
|
|
37
|
-
} from "../chunk-
|
|
41
|
+
} from "../chunk-NAPRGHZ3.js";
|
|
42
|
+
import "../chunk-GIRT7677.js";
|
|
43
|
+
import "../chunk-7J67NYK4.js";
|
|
38
44
|
import {
|
|
39
45
|
rejectUnknownFlags
|
|
40
46
|
} from "../chunk-GMXKITVA.js";
|
|
@@ -50,7 +56,7 @@ import "../chunk-7QLSKOSU.js";
|
|
|
50
56
|
import {
|
|
51
57
|
EXIT
|
|
52
58
|
} from "../chunk-KZERVKTD.js";
|
|
53
|
-
import "../chunk-
|
|
59
|
+
import "../chunk-DMW54VWK.js";
|
|
54
60
|
|
|
55
61
|
// bin/pushary-clean.ts
|
|
56
62
|
import { existsSync, readFileSync, copyFileSync, readdirSync } from "fs";
|
|
@@ -157,11 +163,16 @@ var removeCursorUserHook = () => {
|
|
|
157
163
|
}
|
|
158
164
|
if (!parsed || typeof parsed !== "object") return "failed";
|
|
159
165
|
const hooks = parsed.hooks ?? {};
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
166
|
+
let removedAny = false;
|
|
167
|
+
for (const event of CURSOR_GATE_EVENTS) {
|
|
168
|
+
const existing = Array.isArray(hooks[event]) ? hooks[event] : [];
|
|
169
|
+
const others = existing.filter((h) => !String(h.command ?? "").includes("pushary-gate"));
|
|
170
|
+
if (others.length === existing.length) continue;
|
|
171
|
+
removedAny = true;
|
|
172
|
+
if (others.length === 0) delete hooks[event];
|
|
173
|
+
else hooks[event] = others;
|
|
174
|
+
}
|
|
175
|
+
if (!removedAny) return "absent";
|
|
165
176
|
parsed.hooks = hooks;
|
|
166
177
|
try {
|
|
167
178
|
writeJson(CURSOR_USER_HOOKS, parsed);
|
|
@@ -171,10 +182,12 @@ var removeCursorUserHook = () => {
|
|
|
171
182
|
if (dryRun) return "removed";
|
|
172
183
|
try {
|
|
173
184
|
const after = JSON.parse(readFileSync(CURSOR_USER_HOOKS, "utf-8"));
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
185
|
+
const afterHooks = after?.hooks ?? {};
|
|
186
|
+
const stillThere = CURSOR_GATE_EVENTS.some((event) => {
|
|
187
|
+
const entries = afterHooks[event];
|
|
188
|
+
return Array.isArray(entries) && entries.some((h) => String(h?.command ?? "").includes("pushary-gate"));
|
|
189
|
+
});
|
|
190
|
+
if (stillThere) return "failed";
|
|
178
191
|
} catch {
|
|
179
192
|
return "failed";
|
|
180
193
|
}
|
|
@@ -297,11 +310,15 @@ var main = async () => {
|
|
|
297
310
|
console.log(` ${skip} Skill directory ${dim("(not found)")}`);
|
|
298
311
|
}
|
|
299
312
|
const codexConfig = codexConfigToml();
|
|
313
|
+
const codexHooksJson2 = codexHooksJson();
|
|
314
|
+
const codexHooksData = readJson(codexHooksJson2);
|
|
315
|
+
const codexPositions = codexHookPositions(codexHooksData ?? void 0);
|
|
300
316
|
try {
|
|
301
317
|
const config = readFileSync(codexConfig, "utf-8");
|
|
318
|
+
const parsed = parseTOML(config);
|
|
319
|
+
const trustRemoved = removeCodexHookTrust(parsed, codexHooksJson2, codexPositions);
|
|
302
320
|
const hadPushary = config.includes("pushary");
|
|
303
|
-
if (hadPushary) {
|
|
304
|
-
const parsed = parseTOML(config);
|
|
321
|
+
if (hadPushary || trustRemoved) {
|
|
305
322
|
const mcpServers = parsed.mcp_servers ?? {};
|
|
306
323
|
delete mcpServers.pushary;
|
|
307
324
|
if (Object.keys(mcpServers).length === 0) delete parsed.mcp_servers;
|
|
@@ -311,15 +328,14 @@ var main = async () => {
|
|
|
311
328
|
if (parsed.notify.length === 0) delete parsed.notify;
|
|
312
329
|
}
|
|
313
330
|
guardedWrite(codexConfig, stringifyTOML(parsed));
|
|
314
|
-
|
|
331
|
+
const what = trustRemoved ? "cleaned, hook trust removed" : "cleaned";
|
|
332
|
+
console.log(` ${check} Codex config ${dim(did(what))}`);
|
|
315
333
|
} else {
|
|
316
334
|
console.log(` ${skip} Codex config ${dim("(no pushary entries)")}`);
|
|
317
335
|
}
|
|
318
336
|
} catch {
|
|
319
337
|
console.log(` ${skip} Codex config ${dim("(not found)")}`);
|
|
320
338
|
}
|
|
321
|
-
const codexHooksJson2 = codexHooksJson();
|
|
322
|
-
const codexHooksData = readJson(codexHooksJson2);
|
|
323
339
|
if (codexHooksData) {
|
|
324
340
|
if (removeCodexHooks(codexHooksData)) {
|
|
325
341
|
writeJson(codexHooksJson2, codexHooksData);
|
|
@@ -6,19 +6,20 @@ import {
|
|
|
6
6
|
postLiveSession,
|
|
7
7
|
postLiveSessionUntilAccepted,
|
|
8
8
|
startLiveSessionHeartbeat
|
|
9
|
-
} from "../chunk-
|
|
10
|
-
import "../chunk-
|
|
9
|
+
} from "../chunk-TV3UFGT7.js";
|
|
10
|
+
import "../chunk-QLMX5LLV.js";
|
|
11
11
|
import {
|
|
12
12
|
startProcessOwnership,
|
|
13
13
|
withSpawnProcessOwnership
|
|
14
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-BFBAHOGR.js";
|
|
15
15
|
import {
|
|
16
16
|
spawnClaude
|
|
17
17
|
} from "../chunk-XHKBHWLX.js";
|
|
18
18
|
import {
|
|
19
19
|
resolveBinary
|
|
20
20
|
} from "../chunk-VT4IVERX.js";
|
|
21
|
-
import "../chunk-
|
|
21
|
+
import "../chunk-NAPRGHZ3.js";
|
|
22
|
+
import "../chunk-GIRT7677.js";
|
|
22
23
|
import {
|
|
23
24
|
getMachineId
|
|
24
25
|
} from "../chunk-RN3NOEJF.js";
|
|
@@ -27,7 +28,7 @@ import {
|
|
|
27
28
|
getApiKey,
|
|
28
29
|
getBaseUrl
|
|
29
30
|
} from "../chunk-2UMNXADU.js";
|
|
30
|
-
import "../chunk-
|
|
31
|
+
import "../chunk-DMW54VWK.js";
|
|
31
32
|
|
|
32
33
|
// bin/pushary-codex-bridge.ts
|
|
33
34
|
import { existsSync } from "fs";
|