@librechat/agents 3.1.64 → 3.1.66-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/common/enum.cjs +13 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +3 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/HookRegistry.cjs +162 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -0
- package/dist/cjs/hooks/executeHooks.cjs +276 -0
- package/dist/cjs/hooks/executeHooks.cjs.map +1 -0
- package/dist/cjs/hooks/matchers.cjs +256 -0
- package/dist/cjs/hooks/matchers.cjs.map +1 -0
- package/dist/cjs/hooks/types.cjs +27 -0
- package/dist/cjs/hooks/types.cjs.map +1 -0
- package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +69 -54
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/main.cjs +40 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/core.cjs +8 -1
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +74 -12
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/run.cjs +111 -0
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +175 -0
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -0
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +296 -0
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -0
- package/dist/cjs/tools/ReadFile.cjs +43 -0
- package/dist/cjs/tools/ReadFile.cjs.map +1 -0
- package/dist/cjs/tools/SkillTool.cjs +50 -0
- package/dist/cjs/tools/SkillTool.cjs.map +1 -0
- package/dist/cjs/tools/ToolNode.cjs +304 -140
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/skillCatalog.cjs +84 -0
- package/dist/cjs/tools/skillCatalog.cjs.map +1 -0
- package/dist/esm/common/enum.mjs +12 -1
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +3 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/HookRegistry.mjs +160 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -0
- package/dist/esm/hooks/executeHooks.mjs +273 -0
- package/dist/esm/hooks/executeHooks.mjs.map +1 -0
- package/dist/esm/hooks/matchers.mjs +251 -0
- package/dist/esm/hooks/matchers.mjs.map +1 -0
- package/dist/esm/hooks/types.mjs +25 -0
- package/dist/esm/hooks/types.mjs.map +1 -0
- package/dist/esm/llm/anthropic/types.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +69 -54
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/main.mjs +10 -1
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/core.mjs +8 -1
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +66 -4
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/run.mjs +111 -0
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +169 -0
- package/dist/esm/tools/BashExecutor.mjs.map +1 -0
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +287 -0
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -0
- package/dist/esm/tools/ReadFile.mjs +38 -0
- package/dist/esm/tools/ReadFile.mjs.map +1 -0
- package/dist/esm/tools/SkillTool.mjs +45 -0
- package/dist/esm/tools/SkillTool.mjs.map +1 -0
- package/dist/esm/tools/ToolNode.mjs +306 -142
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/skillCatalog.mjs +82 -0
- package/dist/esm/tools/skillCatalog.mjs.map +1 -0
- package/dist/types/common/enum.d.ts +7 -1
- package/dist/types/graphs/Graph.d.ts +2 -0
- package/dist/types/hooks/HookRegistry.d.ts +56 -0
- package/dist/types/hooks/executeHooks.d.ts +79 -0
- package/dist/types/hooks/index.d.ts +6 -0
- package/dist/types/hooks/matchers.d.ts +95 -0
- package/dist/types/hooks/types.d.ts +309 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/llm/anthropic/types.d.ts +1 -1
- package/dist/types/messages/format.d.ts +2 -1
- package/dist/types/run.d.ts +1 -0
- package/dist/types/tools/BashExecutor.d.ts +45 -0
- package/dist/types/tools/BashProgrammaticToolCalling.d.ts +72 -0
- package/dist/types/tools/ReadFile.d.ts +28 -0
- package/dist/types/tools/SkillTool.d.ts +40 -0
- package/dist/types/tools/ToolNode.d.ts +24 -2
- package/dist/types/tools/skillCatalog.d.ts +19 -0
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/run.d.ts +20 -0
- package/dist/types/types/skill.d.ts +9 -0
- package/dist/types/types/tools.d.ts +38 -1
- package/package.json +2 -2
- package/src/common/enum.ts +12 -0
- package/src/graphs/Graph.ts +4 -0
- package/src/hooks/HookRegistry.ts +208 -0
- package/src/hooks/__tests__/HookRegistry.test.ts +190 -0
- package/src/hooks/__tests__/executeHooks.test.ts +1013 -0
- package/src/hooks/__tests__/integration.test.ts +337 -0
- package/src/hooks/__tests__/matchers.test.ts +238 -0
- package/src/hooks/__tests__/toolHooks.test.ts +669 -0
- package/src/hooks/executeHooks.ts +375 -0
- package/src/hooks/index.ts +55 -0
- package/src/hooks/matchers.ts +280 -0
- package/src/hooks/types.ts +388 -0
- package/src/index.ts +8 -0
- package/src/llm/anthropic/types.ts +1 -1
- package/src/llm/anthropic/utils/message_inputs.ts +93 -68
- package/src/llm/anthropic/utils/server-tool-inputs.test.ts +349 -0
- package/src/messages/core.ts +8 -1
- package/src/messages/format.ts +74 -4
- package/src/messages/formatAgentMessages.skills.test.ts +334 -0
- package/src/run.ts +126 -0
- package/src/tools/BashExecutor.ts +205 -0
- package/src/tools/BashProgrammaticToolCalling.ts +397 -0
- package/src/tools/ReadFile.ts +39 -0
- package/src/tools/SkillTool.ts +46 -0
- package/src/tools/ToolNode.ts +391 -169
- package/src/tools/__tests__/ReadFile.test.ts +44 -0
- package/src/tools/__tests__/SkillTool.test.ts +442 -0
- package/src/tools/__tests__/ToolNode.session.test.ts +12 -12
- package/src/tools/__tests__/skillCatalog.test.ts +161 -0
- package/src/tools/skillCatalog.ts +126 -0
- package/src/types/index.ts +1 -0
- package/src/types/run.ts +20 -0
- package/src/types/skill.ts +11 -0
- package/src/types/tools.ts +41 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Run-scoped storage for hook matchers with an additional layer for
|
|
5
|
+
* session-scoped matchers that should be cleaned up between sessions.
|
|
6
|
+
*
|
|
7
|
+
* Hosts construct one registry per `Run` (mirroring how `HandlerRegistry` is
|
|
8
|
+
* scoped) and register global matchers + per-session matchers against it.
|
|
9
|
+
* Registration is strictly additive — nothing in this class mutates a
|
|
10
|
+
* matcher's callbacks or flags after insertion.
|
|
11
|
+
*
|
|
12
|
+
* ## Why `Map<sessionId, MatcherBucket>` and not `Record`
|
|
13
|
+
*
|
|
14
|
+
* LibreChat runs thousands of parallel sessions in one Node process, and
|
|
15
|
+
* hook registration happens inside hot paths (tool loading, agent spawning).
|
|
16
|
+
* A `Record<sessionId, ...>` has to be spread on every insertion, which is
|
|
17
|
+
* O(n) per call and O(n²) total for a batch of parallel registrations. A
|
|
18
|
+
* Map mutates in place, keeping insertions O(1). This mirrors the reasoning
|
|
19
|
+
* Claude Code documents at `utils/hooks/sessionHooks.ts:62`.
|
|
20
|
+
*/
|
|
21
|
+
class HookRegistry {
|
|
22
|
+
global = {};
|
|
23
|
+
sessions = new Map();
|
|
24
|
+
/**
|
|
25
|
+
* Register a matcher for the lifetime of this registry (= one Run).
|
|
26
|
+
* Returns an unregister function that removes the matcher by reference.
|
|
27
|
+
*/
|
|
28
|
+
register(event, matcher) {
|
|
29
|
+
const list = ensureList(this.global, event);
|
|
30
|
+
list.push(widen(matcher));
|
|
31
|
+
return () => {
|
|
32
|
+
removeFromList(list, matcher);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Register a matcher for a specific session. Cleared automatically when
|
|
37
|
+
* `clearSession(sessionId)` is called, or can be removed directly via the
|
|
38
|
+
* returned unregister function.
|
|
39
|
+
*/
|
|
40
|
+
registerSession(sessionId, event, matcher) {
|
|
41
|
+
const bucket = this.ensureSessionBucket(sessionId);
|
|
42
|
+
const list = ensureList(bucket, event);
|
|
43
|
+
list.push(widen(matcher));
|
|
44
|
+
return () => {
|
|
45
|
+
removeFromList(list, matcher);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Returns all matchers registered for `event`, concatenating global first
|
|
50
|
+
* and then session-specific (when `sessionId` is supplied). The caller
|
|
51
|
+
* receives a fresh array, so iterating it is safe even if a matcher is
|
|
52
|
+
* removed mid-iteration (e.g. via `once: true`).
|
|
53
|
+
*/
|
|
54
|
+
getMatchers(event, sessionId) {
|
|
55
|
+
const globalList = readList(this.global, event);
|
|
56
|
+
if (sessionId === undefined) {
|
|
57
|
+
return snapshot(globalList);
|
|
58
|
+
}
|
|
59
|
+
const bucket = this.sessions.get(sessionId);
|
|
60
|
+
if (bucket === undefined) {
|
|
61
|
+
return snapshot(globalList);
|
|
62
|
+
}
|
|
63
|
+
const sessionList = readList(bucket, event);
|
|
64
|
+
if (globalList.length === 0) {
|
|
65
|
+
return snapshot(sessionList);
|
|
66
|
+
}
|
|
67
|
+
if (sessionList.length === 0) {
|
|
68
|
+
return snapshot(globalList);
|
|
69
|
+
}
|
|
70
|
+
return snapshot([...globalList, ...sessionList]);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Removes `matcher` by reference from global storage first, falling back
|
|
74
|
+
* to the session bucket when `sessionId` is supplied. Used by
|
|
75
|
+
* `executeHooks` to drop `once: true` matchers after they fire.
|
|
76
|
+
*/
|
|
77
|
+
removeMatcher(event, matcher, sessionId) {
|
|
78
|
+
if (removeFromList(readList(this.global, event), matcher)) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if (sessionId === undefined) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const bucket = this.sessions.get(sessionId);
|
|
85
|
+
if (bucket === undefined) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return removeFromList(readList(bucket, event), matcher);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Drops every session-scoped matcher for `sessionId`. Call this in the
|
|
92
|
+
* `finally` block around a Run so a `once: true` hook that never fired
|
|
93
|
+
* cannot leak into the next session on the same registry.
|
|
94
|
+
*/
|
|
95
|
+
clearSession(sessionId) {
|
|
96
|
+
this.sessions.delete(sessionId);
|
|
97
|
+
}
|
|
98
|
+
/** True if at least one matcher exists for `event` (global + session). */
|
|
99
|
+
hasHookFor(event, sessionId) {
|
|
100
|
+
if (readList(this.global, event).length > 0) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
if (sessionId === undefined) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
const bucket = this.sessions.get(sessionId);
|
|
107
|
+
if (bucket === undefined) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return readList(bucket, event).length > 0;
|
|
111
|
+
}
|
|
112
|
+
ensureSessionBucket(sessionId) {
|
|
113
|
+
const existing = this.sessions.get(sessionId);
|
|
114
|
+
if (existing !== undefined) {
|
|
115
|
+
return existing;
|
|
116
|
+
}
|
|
117
|
+
const fresh = {};
|
|
118
|
+
this.sessions.set(sessionId, fresh);
|
|
119
|
+
return fresh;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function ensureList(bucket, event) {
|
|
123
|
+
const existing = bucket[event];
|
|
124
|
+
if (existing !== undefined) {
|
|
125
|
+
return existing;
|
|
126
|
+
}
|
|
127
|
+
const fresh = [];
|
|
128
|
+
bucket[event] = fresh;
|
|
129
|
+
return fresh;
|
|
130
|
+
}
|
|
131
|
+
function readList(bucket, event) {
|
|
132
|
+
return bucket[event] ?? [];
|
|
133
|
+
}
|
|
134
|
+
function removeFromList(list, matcher) {
|
|
135
|
+
const idx = list.indexOf(widen(matcher));
|
|
136
|
+
if (idx < 0) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
list.splice(idx, 1);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Widen a per-event matcher to the storage's uniform slot type. Unsound at
|
|
144
|
+
* the type level (function parameters are contravariant) but safe by
|
|
145
|
+
* construction: `HookRegistry.register<E>` only ever puts matchers into the
|
|
146
|
+
* bucket slot for their own event, and reads go through `snapshot<E>`
|
|
147
|
+
* which is only called with the same `E`.
|
|
148
|
+
*/
|
|
149
|
+
function widen(matcher) {
|
|
150
|
+
return matcher;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Narrow a storage list back to a per-event matcher list on the way out.
|
|
154
|
+
* Sound counterpart to `widen`: the list only contains matchers that were
|
|
155
|
+
* registered against `E`, because the public API enforces it on insert.
|
|
156
|
+
*/
|
|
157
|
+
function snapshot(list) {
|
|
158
|
+
return list.slice();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
exports.HookRegistry = HookRegistry;
|
|
162
|
+
//# sourceMappingURL=HookRegistry.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HookRegistry.cjs","sources":["../../../src/hooks/HookRegistry.ts"],"sourcesContent":["// src/hooks/HookRegistry.ts\nimport type { HookEvent, HookMatcher } from './types';\n\n/**\n * Internal matcher storage type.\n *\n * Matchers registered via the public `register<E>` API are strictly typed\n * to a single `E`, but the storage needs one uniform slot type per event.\n * We store them as `HookMatcher<HookEvent>` and cast once at the variance\n * boundary — see `ensureList` and `snapshot` below. The invariant (every\n * matcher in `bucket[event]` was registered with that exact event) is\n * enforced by the public API; breaking it requires bypassing the types.\n */\ntype MatcherBucket = Partial<Record<HookEvent, HookMatcher<HookEvent>[]>>;\n\n/**\n * Run-scoped storage for hook matchers with an additional layer for\n * session-scoped matchers that should be cleaned up between sessions.\n *\n * Hosts construct one registry per `Run` (mirroring how `HandlerRegistry` is\n * scoped) and register global matchers + per-session matchers against it.\n * Registration is strictly additive — nothing in this class mutates a\n * matcher's callbacks or flags after insertion.\n *\n * ## Why `Map<sessionId, MatcherBucket>` and not `Record`\n *\n * LibreChat runs thousands of parallel sessions in one Node process, and\n * hook registration happens inside hot paths (tool loading, agent spawning).\n * A `Record<sessionId, ...>` has to be spread on every insertion, which is\n * O(n) per call and O(n²) total for a batch of parallel registrations. A\n * Map mutates in place, keeping insertions O(1). This mirrors the reasoning\n * Claude Code documents at `utils/hooks/sessionHooks.ts:62`.\n */\nexport class HookRegistry {\n private readonly global: MatcherBucket = {};\n private readonly sessions: Map<string, MatcherBucket> = new Map();\n\n /**\n * Register a matcher for the lifetime of this registry (= one Run).\n * Returns an unregister function that removes the matcher by reference.\n */\n register<E extends HookEvent>(event: E, matcher: HookMatcher<E>): () => void {\n const list = ensureList(this.global, event);\n list.push(widen(matcher));\n return () => {\n removeFromList(list, matcher);\n };\n }\n\n /**\n * Register a matcher for a specific session. Cleared automatically when\n * `clearSession(sessionId)` is called, or can be removed directly via the\n * returned unregister function.\n */\n registerSession<E extends HookEvent>(\n sessionId: string,\n event: E,\n matcher: HookMatcher<E>\n ): () => void {\n const bucket = this.ensureSessionBucket(sessionId);\n const list = ensureList(bucket, event);\n list.push(widen(matcher));\n return () => {\n removeFromList(list, matcher);\n };\n }\n\n /**\n * Returns all matchers registered for `event`, concatenating global first\n * and then session-specific (when `sessionId` is supplied). The caller\n * receives a fresh array, so iterating it is safe even if a matcher is\n * removed mid-iteration (e.g. via `once: true`).\n */\n getMatchers<E extends HookEvent>(\n event: E,\n sessionId?: string\n ): HookMatcher<E>[] {\n const globalList = readList(this.global, event);\n if (sessionId === undefined) {\n return snapshot<E>(globalList);\n }\n const bucket = this.sessions.get(sessionId);\n if (bucket === undefined) {\n return snapshot<E>(globalList);\n }\n const sessionList = readList(bucket, event);\n if (globalList.length === 0) {\n return snapshot<E>(sessionList);\n }\n if (sessionList.length === 0) {\n return snapshot<E>(globalList);\n }\n return snapshot<E>([...globalList, ...sessionList]);\n }\n\n /**\n * Removes `matcher` by reference from global storage first, falling back\n * to the session bucket when `sessionId` is supplied. Used by\n * `executeHooks` to drop `once: true` matchers after they fire.\n */\n removeMatcher<E extends HookEvent>(\n event: E,\n matcher: HookMatcher<E>,\n sessionId?: string\n ): boolean {\n if (removeFromList(readList(this.global, event), matcher)) {\n return true;\n }\n if (sessionId === undefined) {\n return false;\n }\n const bucket = this.sessions.get(sessionId);\n if (bucket === undefined) {\n return false;\n }\n return removeFromList(readList(bucket, event), matcher);\n }\n\n /**\n * Drops every session-scoped matcher for `sessionId`. Call this in the\n * `finally` block around a Run so a `once: true` hook that never fired\n * cannot leak into the next session on the same registry.\n */\n clearSession(sessionId: string): void {\n this.sessions.delete(sessionId);\n }\n\n /** True if at least one matcher exists for `event` (global + session). */\n hasHookFor(event: HookEvent, sessionId?: string): boolean {\n if (readList(this.global, event).length > 0) {\n return true;\n }\n if (sessionId === undefined) {\n return false;\n }\n const bucket = this.sessions.get(sessionId);\n if (bucket === undefined) {\n return false;\n }\n return readList(bucket, event).length > 0;\n }\n\n private ensureSessionBucket(sessionId: string): MatcherBucket {\n const existing = this.sessions.get(sessionId);\n if (existing !== undefined) {\n return existing;\n }\n const fresh: MatcherBucket = {};\n this.sessions.set(sessionId, fresh);\n return fresh;\n }\n}\n\nfunction ensureList(\n bucket: MatcherBucket,\n event: HookEvent\n): HookMatcher<HookEvent>[] {\n const existing = bucket[event];\n if (existing !== undefined) {\n return existing;\n }\n const fresh: HookMatcher<HookEvent>[] = [];\n bucket[event] = fresh;\n return fresh;\n}\n\nfunction readList(\n bucket: MatcherBucket,\n event: HookEvent\n): HookMatcher<HookEvent>[] {\n return bucket[event] ?? [];\n}\n\nfunction removeFromList<E extends HookEvent>(\n list: HookMatcher<HookEvent>[],\n matcher: HookMatcher<E>\n): boolean {\n const idx = list.indexOf(widen(matcher));\n if (idx < 0) {\n return false;\n }\n list.splice(idx, 1);\n return true;\n}\n\n/**\n * Widen a per-event matcher to the storage's uniform slot type. Unsound at\n * the type level (function parameters are contravariant) but safe by\n * construction: `HookRegistry.register<E>` only ever puts matchers into the\n * bucket slot for their own event, and reads go through `snapshot<E>`\n * which is only called with the same `E`.\n */\nfunction widen<E extends HookEvent>(\n matcher: HookMatcher<E>\n): HookMatcher<HookEvent> {\n return matcher as unknown as HookMatcher<HookEvent>;\n}\n\n/**\n * Narrow a storage list back to a per-event matcher list on the way out.\n * Sound counterpart to `widen`: the list only contains matchers that were\n * registered against `E`, because the public API enforces it on insert.\n */\nfunction snapshot<E extends HookEvent>(\n list: readonly HookMatcher<HookEvent>[]\n): HookMatcher<E>[] {\n return list.slice() as unknown as HookMatcher<E>[];\n}\n"],"names":[],"mappings":";;AAeA;;;;;;;;;;;;;;;;;AAiBG;MACU,YAAY,CAAA;IACN,MAAM,GAAkB,EAAE;AAC1B,IAAA,QAAQ,GAA+B,IAAI,GAAG,EAAE;AAEjE;;;AAGG;IACH,QAAQ,CAAsB,KAAQ,EAAE,OAAuB,EAAA;QAC7D,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;QAC3C,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;AACzB,QAAA,OAAO,MAAK;AACV,YAAA,cAAc,CAAC,IAAI,EAAE,OAAO,CAAC;AAC/B,QAAA,CAAC;IACH;AAEA;;;;AAIG;AACH,IAAA,eAAe,CACb,SAAiB,EACjB,KAAQ,EACR,OAAuB,EAAA;QAEvB,MAAM,MAAM,GAAG,IAAI,CAAC,mBAAmB,CAAC,SAAS,CAAC;QAClD,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,EAAE,KAAK,CAAC;QACtC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;AACzB,QAAA,OAAO,MAAK;AACV,YAAA,cAAc,CAAC,IAAI,EAAE,OAAO,CAAC;AAC/B,QAAA,CAAC;IACH;AAEA;;;;;AAKG;IACH,WAAW,CACT,KAAQ,EACR,SAAkB,EAAA;QAElB,MAAM,UAAU,GAAG,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;AAC/C,QAAA,IAAI,SAAS,KAAK,SAAS,EAAE;AAC3B,YAAA,OAAO,QAAQ,CAAI,UAAU,CAAC;QAChC;QACA,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC;AAC3C,QAAA,IAAI,MAAM,KAAK,SAAS,EAAE;AACxB,YAAA,OAAO,QAAQ,CAAI,UAAU,CAAC;QAChC;QACA,MAAM,WAAW,GAAG,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;AAC3C,QAAA,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE;AAC3B,YAAA,OAAO,QAAQ,CAAI,WAAW,CAAC;QACjC;AACA,QAAA,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE;AAC5B,YAAA,OAAO,QAAQ,CAAI,UAAU,CAAC;QAChC;QACA,OAAO,QAAQ,CAAI,CAAC,GAAG,UAAU,EAAE,GAAG,WAAW,CAAC,CAAC;IACrD;AAEA;;;;AAIG;AACH,IAAA,aAAa,CACX,KAAQ,EACR,OAAuB,EACvB,SAAkB,EAAA;AAElB,QAAA,IAAI,cAAc,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,EAAE;AACzD,YAAA,OAAO,IAAI;QACb;AACA,QAAA,IAAI,SAAS,KAAK,SAAS,EAAE;AAC3B,YAAA,OAAO,KAAK;QACd;QACA,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC;AAC3C,QAAA,IAAI,MAAM,KAAK,SAAS,EAAE;AACxB,YAAA,OAAO,KAAK;QACd;QACA,OAAO,cAAc,CAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IACzD;AAEA;;;;AAIG;AACH,IAAA,YAAY,CAAC,SAAiB,EAAA;AAC5B,QAAA,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC;IACjC;;IAGA,UAAU,CAAC,KAAgB,EAAE,SAAkB,EAAA;AAC7C,QAAA,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE;AAC3C,YAAA,OAAO,IAAI;QACb;AACA,QAAA,IAAI,SAAS,KAAK,SAAS,EAAE;AAC3B,YAAA,OAAO,KAAK;QACd;QACA,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC;AAC3C,QAAA,IAAI,MAAM,KAAK,SAAS,EAAE;AACxB,YAAA,OAAO,KAAK;QACd;QACA,OAAO,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC;IAC3C;AAEQ,IAAA,mBAAmB,CAAC,SAAiB,EAAA;QAC3C,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC;AAC7C,QAAA,IAAI,QAAQ,KAAK,SAAS,EAAE;AAC1B,YAAA,OAAO,QAAQ;QACjB;QACA,MAAM,KAAK,GAAkB,EAAE;QAC/B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,CAAC;AACnC,QAAA,OAAO,KAAK;IACd;AACD;AAED,SAAS,UAAU,CACjB,MAAqB,EACrB,KAAgB,EAAA;AAEhB,IAAA,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC;AAC9B,IAAA,IAAI,QAAQ,KAAK,SAAS,EAAE;AAC1B,QAAA,OAAO,QAAQ;IACjB;IACA,MAAM,KAAK,GAA6B,EAAE;AAC1C,IAAA,MAAM,CAAC,KAAK,CAAC,GAAG,KAAK;AACrB,IAAA,OAAO,KAAK;AACd;AAEA,SAAS,QAAQ,CACf,MAAqB,EACrB,KAAgB,EAAA;AAEhB,IAAA,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE;AAC5B;AAEA,SAAS,cAAc,CACrB,IAA8B,EAC9B,OAAuB,EAAA;IAEvB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;AACxC,IAAA,IAAI,GAAG,GAAG,CAAC,EAAE;AACX,QAAA,OAAO,KAAK;IACd;AACA,IAAA,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;AACnB,IAAA,OAAO,IAAI;AACb;AAEA;;;;;;AAMG;AACH,SAAS,KAAK,CACZ,OAAuB,EAAA;AAEvB,IAAA,OAAO,OAA4C;AACrD;AAEA;;;;AAIG;AACH,SAAS,QAAQ,CACf,IAAuC,EAAA;AAEvC,IAAA,OAAO,IAAI,CAAC,KAAK,EAAiC;AACpD;;;;"}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var matchers = require('./matchers.cjs');
|
|
4
|
+
|
|
5
|
+
/** Default per-hook timeout when a matcher doesn't set its own. */
|
|
6
|
+
const DEFAULT_HOOK_TIMEOUT_MS = 30_000;
|
|
7
|
+
function freshResult() {
|
|
8
|
+
return {
|
|
9
|
+
additionalContexts: [],
|
|
10
|
+
errors: [],
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function combineSignals(parent, timeoutMs) {
|
|
14
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
15
|
+
if (parent === undefined) {
|
|
16
|
+
return timeoutSignal;
|
|
17
|
+
}
|
|
18
|
+
return AbortSignal.any([parent, timeoutSignal]);
|
|
19
|
+
}
|
|
20
|
+
function isTimeout(err) {
|
|
21
|
+
if (err instanceof Error) {
|
|
22
|
+
return err.name === 'TimeoutError' || err.name === 'AbortError';
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
function describeError(err) {
|
|
27
|
+
if (err instanceof Error) {
|
|
28
|
+
return err.message !== '' ? err.message : err.name;
|
|
29
|
+
}
|
|
30
|
+
return String(err);
|
|
31
|
+
}
|
|
32
|
+
function makeAbortPromise(signal) {
|
|
33
|
+
let onAbort;
|
|
34
|
+
const promise = new Promise((_resolve, reject) => {
|
|
35
|
+
if (signal.aborted) {
|
|
36
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error('aborted'));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
onAbort = () => {
|
|
40
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error('aborted'));
|
|
41
|
+
};
|
|
42
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
43
|
+
});
|
|
44
|
+
const cleanup = () => {
|
|
45
|
+
if (onAbort !== undefined) {
|
|
46
|
+
signal.removeEventListener('abort', onAbort);
|
|
47
|
+
onAbort = undefined;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
return { promise, cleanup };
|
|
51
|
+
}
|
|
52
|
+
async function runHook(hook, input, signal, matcher) {
|
|
53
|
+
const hookPromise = Promise.resolve().then(() => hook(input, signal));
|
|
54
|
+
const { promise: abortPromise, cleanup } = makeAbortPromise(signal);
|
|
55
|
+
try {
|
|
56
|
+
const output = await Promise.race([hookPromise, abortPromise]);
|
|
57
|
+
return { matcher, output, error: null, timedOut: false };
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
return {
|
|
61
|
+
matcher,
|
|
62
|
+
output: null,
|
|
63
|
+
error: describeError(err),
|
|
64
|
+
timedOut: isTimeout(err),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
cleanup();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function reportErrors(outcomes, event, logger) {
|
|
72
|
+
for (const outcome of outcomes) {
|
|
73
|
+
if (outcome.error === null) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (outcome.matcher.internal === true) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const label = outcome.timedOut ? 'timed out' : 'threw an error';
|
|
80
|
+
const message = `Hook for ${event} ${label}: ${outcome.error}`;
|
|
81
|
+
if (logger !== undefined) {
|
|
82
|
+
logger.warn(message);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
// eslint-disable-next-line no-console
|
|
86
|
+
console.warn(message);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function applyToolDecision(agg, decision, reason) {
|
|
90
|
+
if (decision === 'deny') {
|
|
91
|
+
if (agg.decision === 'deny') {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
agg.decision = 'deny';
|
|
95
|
+
agg.reason = reason;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (decision === 'ask') {
|
|
99
|
+
if (agg.decision === 'deny' || agg.decision === 'ask') {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
agg.decision = 'ask';
|
|
103
|
+
agg.reason = reason;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (agg.decision === undefined) {
|
|
107
|
+
agg.decision = 'allow';
|
|
108
|
+
agg.reason = reason;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function applyStopDecision(agg, decision, reason) {
|
|
112
|
+
if (decision === 'block') {
|
|
113
|
+
if (agg.stopDecision === 'block') {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
agg.stopDecision = 'block';
|
|
117
|
+
agg.reason = reason;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (agg.stopDecision === undefined) {
|
|
121
|
+
agg.stopDecision = 'continue';
|
|
122
|
+
if (agg.reason === undefined) {
|
|
123
|
+
agg.reason = reason;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function applyDecision(agg, output) {
|
|
128
|
+
if (!('decision' in output) || output.decision === undefined) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const decision = output.decision;
|
|
132
|
+
const reason = 'reason' in output && typeof output.reason === 'string'
|
|
133
|
+
? output.reason
|
|
134
|
+
: undefined;
|
|
135
|
+
if (decision === 'deny' || decision === 'ask' || decision === 'allow') {
|
|
136
|
+
applyToolDecision(agg, decision, reason);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
applyStopDecision(agg, decision, reason);
|
|
140
|
+
}
|
|
141
|
+
function applyContext(agg, output) {
|
|
142
|
+
if (typeof output.additionalContext === 'string' &&
|
|
143
|
+
output.additionalContext.length > 0) {
|
|
144
|
+
agg.additionalContexts.push(output.additionalContext);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function applyStopFlag(agg, output) {
|
|
148
|
+
if (output.preventContinuation !== true) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
agg.preventContinuation = true;
|
|
152
|
+
if (typeof output.stopReason === 'string' && agg.stopReason === undefined) {
|
|
153
|
+
agg.stopReason = output.stopReason;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function applyUpdatedInput(agg, output) {
|
|
157
|
+
if (!('updatedInput' in output) || output.updatedInput === undefined) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
agg.updatedInput = output.updatedInput;
|
|
161
|
+
}
|
|
162
|
+
function applyUpdatedOutput(agg, output) {
|
|
163
|
+
if (!('updatedOutput' in output) || output.updatedOutput === undefined) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
agg.updatedOutput = output.updatedOutput;
|
|
167
|
+
}
|
|
168
|
+
function fold(outcomes) {
|
|
169
|
+
const agg = freshResult();
|
|
170
|
+
for (const outcome of outcomes) {
|
|
171
|
+
if (outcome.error !== null) {
|
|
172
|
+
if (outcome.matcher.internal !== true) {
|
|
173
|
+
agg.errors.push(outcome.error);
|
|
174
|
+
}
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const output = outcome.output;
|
|
178
|
+
if (output === null) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
applyContext(agg, output);
|
|
182
|
+
applyStopFlag(agg, output);
|
|
183
|
+
applyDecision(agg, output);
|
|
184
|
+
applyUpdatedInput(agg, output);
|
|
185
|
+
applyUpdatedOutput(agg, output);
|
|
186
|
+
}
|
|
187
|
+
return agg;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Fires every matcher registered against `input.hook_event_name`, folding
|
|
191
|
+
* their results per `deny > ask > allow` precedence and accumulating
|
|
192
|
+
* context/errors.
|
|
193
|
+
*
|
|
194
|
+
* ## Parallelism and determinism
|
|
195
|
+
*
|
|
196
|
+
* All matching hooks fire simultaneously and are awaited via `Promise.all`,
|
|
197
|
+
* which preserves input-array order in its returned results. The fold
|
|
198
|
+
* therefore iterates outcomes in **registration order** — outer loop over
|
|
199
|
+
* matchers as they sit in the registry (global first, then session), inner
|
|
200
|
+
* loop over each matcher's `hooks` array. Last-writer-wins fields
|
|
201
|
+
* (`updatedInput`, `updatedOutput`) are deterministic in that order, even
|
|
202
|
+
* though hooks may complete in arbitrary wall-clock order.
|
|
203
|
+
*
|
|
204
|
+
* Consumers that need a single authoritative rewrite should still scope
|
|
205
|
+
* `updatedInput`/`updatedOutput` to one hook per matcher to avoid subtle
|
|
206
|
+
* precedence bugs when matchers are added in a different order than
|
|
207
|
+
* expected.
|
|
208
|
+
*
|
|
209
|
+
* ## Timeouts and cancellation
|
|
210
|
+
*
|
|
211
|
+
* Each matcher receives **one shared `AbortSignal`** derived from the
|
|
212
|
+
* caller's parent signal combined with `matcher.timeout` (falling back to
|
|
213
|
+
* `opts.timeoutMs`, default {@link DEFAULT_HOOK_TIMEOUT_MS}). Sharing the
|
|
214
|
+
* signal across hooks in a matcher collapses N timer allocations into
|
|
215
|
+
* one, which matters on the PreToolUse hot path where a matcher with
|
|
216
|
+
* several hooks fires on every tool call. Each hook call is raced
|
|
217
|
+
* against the shared signal, so even a hook that ignores the signal is
|
|
218
|
+
* force-unblocked when the timeout fires. Timeout/abort errors are
|
|
219
|
+
* swallowed into the aggregated result's `errors` array (non-fatal by
|
|
220
|
+
* default).
|
|
221
|
+
*
|
|
222
|
+
* ## Internal matchers
|
|
223
|
+
*
|
|
224
|
+
* A matcher with `internal: true` is excluded from both the `errors` array
|
|
225
|
+
* and the logger output. Use it for infrastructure hooks whose failures
|
|
226
|
+
* should not pollute user-visible diagnostics.
|
|
227
|
+
*
|
|
228
|
+
* ## Once semantics — atomic at-most-once
|
|
229
|
+
*
|
|
230
|
+
* A matcher with `once: true` is removed from the registry **before any
|
|
231
|
+
* hook runs**, inside the synchronous prefix of `executeHooks` (between
|
|
232
|
+
* `getMatchers` and the first `await`). Because Node's event loop serialises
|
|
233
|
+
* sync work, two concurrent `executeHooks` calls can never both observe
|
|
234
|
+
* and dispatch the same `once` matcher — whichever call runs its sync
|
|
235
|
+
* prefix first consumes it, and the loser sees an empty bucket.
|
|
236
|
+
*
|
|
237
|
+
* Trade-off: if every hook in a `once` matcher throws, the matcher is
|
|
238
|
+
* still gone. "Once" here means "at most one dispatch, ever", not "at
|
|
239
|
+
* most one successful execution with retry on failure". Hosts that need
|
|
240
|
+
* retry semantics should register a normal matcher and self-unregister
|
|
241
|
+
* via the `unregister` callback returned from `registry.register`.
|
|
242
|
+
*/
|
|
243
|
+
async function executeHooks(opts) {
|
|
244
|
+
const { registry, input, sessionId, matchQuery, signal, timeoutMs = DEFAULT_HOOK_TIMEOUT_MS, logger, } = opts;
|
|
245
|
+
const event = input.hook_event_name;
|
|
246
|
+
const matchers$1 = registry.getMatchers(event, sessionId);
|
|
247
|
+
if (matchers$1.length === 0) {
|
|
248
|
+
return freshResult();
|
|
249
|
+
}
|
|
250
|
+
// --- SYNC CRITICAL SECTION: once-matcher removal must complete before any await ---
|
|
251
|
+
const tasks = [];
|
|
252
|
+
for (const matcher of matchers$1) {
|
|
253
|
+
if (!matchers.matchesQuery(matcher.pattern, matchQuery)) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (matcher.once === true) {
|
|
257
|
+
registry.removeMatcher(event, matcher, sessionId);
|
|
258
|
+
}
|
|
259
|
+
const perHookTimeout = matcher.timeout ?? timeoutMs;
|
|
260
|
+
const matcherSignal = combineSignals(signal, perHookTimeout);
|
|
261
|
+
for (const hook of matcher.hooks) {
|
|
262
|
+
tasks.push(runHook(hook, input, matcherSignal, matcher));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// --- END SYNC CRITICAL SECTION ---
|
|
266
|
+
if (tasks.length === 0) {
|
|
267
|
+
return freshResult();
|
|
268
|
+
}
|
|
269
|
+
const outcomes = await Promise.all(tasks);
|
|
270
|
+
reportErrors(outcomes, event, logger);
|
|
271
|
+
return fold(outcomes);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
exports.DEFAULT_HOOK_TIMEOUT_MS = DEFAULT_HOOK_TIMEOUT_MS;
|
|
275
|
+
exports.executeHooks = executeHooks;
|
|
276
|
+
//# sourceMappingURL=executeHooks.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executeHooks.cjs","sources":["../../../src/hooks/executeHooks.ts"],"sourcesContent":["// src/hooks/executeHooks.ts\nimport type { Logger } from 'winston';\nimport type { HookRegistry } from './HookRegistry';\nimport type {\n HookInput,\n HookEvent,\n HookOutput,\n HookMatcher,\n ToolDecision,\n StopDecision,\n HookCallback,\n AggregatedHookResult,\n} from './types';\nimport { matchesQuery } from './matchers';\n\n/** Default per-hook timeout when a matcher doesn't set its own. */\nexport const DEFAULT_HOOK_TIMEOUT_MS = 30_000;\n\n/**\n * Options for a single `executeHooks` call. The `input` drives everything —\n * the event name is read from `input.hook_event_name`, matchers are looked\n * up against that event, and each hook receives `input` directly.\n */\nexport interface ExecuteHooksOptions {\n registry: HookRegistry;\n input: HookInput;\n /** Scope lookup to this session (in addition to global matchers). */\n sessionId?: string;\n /** Query string matched against each matcher's pattern (tool name, etc.). */\n matchQuery?: string;\n /** Parent AbortSignal — combined with per-hook timeout into the hook signal. */\n signal?: AbortSignal;\n /** Default per-hook timeout; overridden by `matcher.timeout` when present. */\n timeoutMs?: number;\n /** Optional winston logger for non-internal hook errors. */\n logger?: Logger;\n}\n\ntype WideMatcher = HookMatcher<HookEvent>;\ntype WideCallback = HookCallback<HookEvent>;\n\ninterface HookOutcome {\n matcher: WideMatcher;\n output: HookOutput | null;\n error: string | null;\n timedOut: boolean;\n}\n\nfunction freshResult(): AggregatedHookResult {\n return {\n additionalContexts: [],\n errors: [],\n };\n}\n\nfunction combineSignals(\n parent: AbortSignal | undefined,\n timeoutMs: number\n): AbortSignal {\n const timeoutSignal = AbortSignal.timeout(timeoutMs);\n if (parent === undefined) {\n return timeoutSignal;\n }\n return AbortSignal.any([parent, timeoutSignal]);\n}\n\nfunction isTimeout(err: unknown): boolean {\n if (err instanceof Error) {\n return err.name === 'TimeoutError' || err.name === 'AbortError';\n }\n return false;\n}\n\nfunction describeError(err: unknown): string {\n if (err instanceof Error) {\n return err.message !== '' ? err.message : err.name;\n }\n return String(err);\n}\n\nfunction makeAbortPromise(signal: AbortSignal): {\n promise: Promise<never>;\n cleanup: () => void;\n} {\n let onAbort: (() => void) | undefined;\n const promise = new Promise<never>((_resolve, reject) => {\n if (signal.aborted) {\n reject(\n signal.reason instanceof Error ? signal.reason : new Error('aborted')\n );\n return;\n }\n onAbort = (): void => {\n reject(\n signal.reason instanceof Error ? signal.reason : new Error('aborted')\n );\n };\n signal.addEventListener('abort', onAbort, { once: true });\n });\n const cleanup = (): void => {\n if (onAbort !== undefined) {\n signal.removeEventListener('abort', onAbort);\n onAbort = undefined;\n }\n };\n return { promise, cleanup };\n}\n\nasync function runHook(\n hook: WideCallback,\n input: HookInput,\n signal: AbortSignal,\n matcher: WideMatcher\n): Promise<HookOutcome> {\n const hookPromise = Promise.resolve().then(() => hook(input, signal));\n const { promise: abortPromise, cleanup } = makeAbortPromise(signal);\n try {\n const output = await Promise.race([hookPromise, abortPromise]);\n return { matcher, output, error: null, timedOut: false };\n } catch (err) {\n return {\n matcher,\n output: null,\n error: describeError(err),\n timedOut: isTimeout(err),\n };\n } finally {\n cleanup();\n }\n}\n\nfunction reportErrors(\n outcomes: readonly HookOutcome[],\n event: HookEvent,\n logger: Logger | undefined\n): void {\n for (const outcome of outcomes) {\n if (outcome.error === null) {\n continue;\n }\n if (outcome.matcher.internal === true) {\n continue;\n }\n const label = outcome.timedOut ? 'timed out' : 'threw an error';\n const message = `Hook for ${event} ${label}: ${outcome.error}`;\n if (logger !== undefined) {\n logger.warn(message);\n continue;\n }\n // eslint-disable-next-line no-console\n console.warn(message);\n }\n}\n\nfunction applyToolDecision(\n agg: AggregatedHookResult,\n decision: ToolDecision,\n reason: string | undefined\n): void {\n if (decision === 'deny') {\n if (agg.decision === 'deny') {\n return;\n }\n agg.decision = 'deny';\n agg.reason = reason;\n return;\n }\n if (decision === 'ask') {\n if (agg.decision === 'deny' || agg.decision === 'ask') {\n return;\n }\n agg.decision = 'ask';\n agg.reason = reason;\n return;\n }\n if (agg.decision === undefined) {\n agg.decision = 'allow';\n agg.reason = reason;\n }\n}\n\nfunction applyStopDecision(\n agg: AggregatedHookResult,\n decision: StopDecision,\n reason: string | undefined\n): void {\n if (decision === 'block') {\n if (agg.stopDecision === 'block') {\n return;\n }\n agg.stopDecision = 'block';\n agg.reason = reason;\n return;\n }\n if (agg.stopDecision === undefined) {\n agg.stopDecision = 'continue';\n if (agg.reason === undefined) {\n agg.reason = reason;\n }\n }\n}\n\nfunction applyDecision(agg: AggregatedHookResult, output: HookOutput): void {\n if (!('decision' in output) || output.decision === undefined) {\n return;\n }\n const decision = output.decision;\n const reason =\n 'reason' in output && typeof output.reason === 'string'\n ? output.reason\n : undefined;\n if (decision === 'deny' || decision === 'ask' || decision === 'allow') {\n applyToolDecision(agg, decision, reason);\n return;\n }\n applyStopDecision(agg, decision, reason);\n}\n\nfunction applyContext(agg: AggregatedHookResult, output: HookOutput): void {\n if (\n typeof output.additionalContext === 'string' &&\n output.additionalContext.length > 0\n ) {\n agg.additionalContexts.push(output.additionalContext);\n }\n}\n\nfunction applyStopFlag(agg: AggregatedHookResult, output: HookOutput): void {\n if (output.preventContinuation !== true) {\n return;\n }\n agg.preventContinuation = true;\n if (typeof output.stopReason === 'string' && agg.stopReason === undefined) {\n agg.stopReason = output.stopReason;\n }\n}\n\nfunction applyUpdatedInput(\n agg: AggregatedHookResult,\n output: HookOutput\n): void {\n if (!('updatedInput' in output) || output.updatedInput === undefined) {\n return;\n }\n agg.updatedInput = output.updatedInput;\n}\n\nfunction applyUpdatedOutput(\n agg: AggregatedHookResult,\n output: HookOutput\n): void {\n if (!('updatedOutput' in output) || output.updatedOutput === undefined) {\n return;\n }\n agg.updatedOutput = output.updatedOutput;\n}\n\nfunction fold(outcomes: readonly HookOutcome[]): AggregatedHookResult {\n const agg = freshResult();\n for (const outcome of outcomes) {\n if (outcome.error !== null) {\n if (outcome.matcher.internal !== true) {\n agg.errors.push(outcome.error);\n }\n continue;\n }\n const output = outcome.output;\n if (output === null) {\n continue;\n }\n applyContext(agg, output);\n applyStopFlag(agg, output);\n applyDecision(agg, output);\n applyUpdatedInput(agg, output);\n applyUpdatedOutput(agg, output);\n }\n return agg;\n}\n\n/**\n * Fires every matcher registered against `input.hook_event_name`, folding\n * their results per `deny > ask > allow` precedence and accumulating\n * context/errors.\n *\n * ## Parallelism and determinism\n *\n * All matching hooks fire simultaneously and are awaited via `Promise.all`,\n * which preserves input-array order in its returned results. The fold\n * therefore iterates outcomes in **registration order** — outer loop over\n * matchers as they sit in the registry (global first, then session), inner\n * loop over each matcher's `hooks` array. Last-writer-wins fields\n * (`updatedInput`, `updatedOutput`) are deterministic in that order, even\n * though hooks may complete in arbitrary wall-clock order.\n *\n * Consumers that need a single authoritative rewrite should still scope\n * `updatedInput`/`updatedOutput` to one hook per matcher to avoid subtle\n * precedence bugs when matchers are added in a different order than\n * expected.\n *\n * ## Timeouts and cancellation\n *\n * Each matcher receives **one shared `AbortSignal`** derived from the\n * caller's parent signal combined with `matcher.timeout` (falling back to\n * `opts.timeoutMs`, default {@link DEFAULT_HOOK_TIMEOUT_MS}). Sharing the\n * signal across hooks in a matcher collapses N timer allocations into\n * one, which matters on the PreToolUse hot path where a matcher with\n * several hooks fires on every tool call. Each hook call is raced\n * against the shared signal, so even a hook that ignores the signal is\n * force-unblocked when the timeout fires. Timeout/abort errors are\n * swallowed into the aggregated result's `errors` array (non-fatal by\n * default).\n *\n * ## Internal matchers\n *\n * A matcher with `internal: true` is excluded from both the `errors` array\n * and the logger output. Use it for infrastructure hooks whose failures\n * should not pollute user-visible diagnostics.\n *\n * ## Once semantics — atomic at-most-once\n *\n * A matcher with `once: true` is removed from the registry **before any\n * hook runs**, inside the synchronous prefix of `executeHooks` (between\n * `getMatchers` and the first `await`). Because Node's event loop serialises\n * sync work, two concurrent `executeHooks` calls can never both observe\n * and dispatch the same `once` matcher — whichever call runs its sync\n * prefix first consumes it, and the loser sees an empty bucket.\n *\n * Trade-off: if every hook in a `once` matcher throws, the matcher is\n * still gone. \"Once\" here means \"at most one dispatch, ever\", not \"at\n * most one successful execution with retry on failure\". Hosts that need\n * retry semantics should register a normal matcher and self-unregister\n * via the `unregister` callback returned from `registry.register`.\n */\nexport async function executeHooks(\n opts: ExecuteHooksOptions\n): Promise<AggregatedHookResult> {\n const {\n registry,\n input,\n sessionId,\n matchQuery,\n signal,\n timeoutMs = DEFAULT_HOOK_TIMEOUT_MS,\n logger,\n } = opts;\n const event = input.hook_event_name;\n const matchers = registry.getMatchers(event, sessionId);\n if (matchers.length === 0) {\n return freshResult();\n }\n\n // --- SYNC CRITICAL SECTION: once-matcher removal must complete before any await ---\n const tasks: Promise<HookOutcome>[] = [];\n for (const matcher of matchers) {\n if (!matchesQuery(matcher.pattern, matchQuery)) {\n continue;\n }\n if (matcher.once === true) {\n registry.removeMatcher(event, matcher, sessionId);\n }\n const perHookTimeout = matcher.timeout ?? timeoutMs;\n const matcherSignal = combineSignals(signal, perHookTimeout);\n for (const hook of matcher.hooks) {\n tasks.push(runHook(hook, input, matcherSignal, matcher));\n }\n }\n // --- END SYNC CRITICAL SECTION ---\n if (tasks.length === 0) {\n return freshResult();\n }\n\n const outcomes = await Promise.all(tasks);\n reportErrors(outcomes, event, logger);\n return fold(outcomes);\n}\n"],"names":["matchers","matchesQuery"],"mappings":"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"}
|