agenthooksprotocol 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +69 -2
  3. package/dist/src/client/auth.d.ts +147 -0
  4. package/dist/src/client/auth.d.ts.map +1 -0
  5. package/dist/src/client/auth.js +629 -0
  6. package/dist/src/client/auth.js.map +1 -0
  7. package/dist/src/client/composition.d.ts +44 -0
  8. package/dist/src/client/composition.d.ts.map +1 -0
  9. package/dist/src/client/composition.js +531 -0
  10. package/dist/src/client/composition.js.map +1 -0
  11. package/dist/src/client/content.d.ts +75 -0
  12. package/dist/src/client/content.d.ts.map +1 -0
  13. package/dist/src/client/content.js +415 -0
  14. package/dist/src/client/content.js.map +1 -0
  15. package/dist/src/client/hooks.d.ts +66 -0
  16. package/dist/src/client/hooks.d.ts.map +1 -0
  17. package/dist/src/client/hooks.js +1084 -0
  18. package/dist/src/client/hooks.js.map +1 -0
  19. package/dist/src/client/index.d.ts +12 -0
  20. package/dist/src/client/index.d.ts.map +1 -0
  21. package/dist/src/client/index.js +10 -0
  22. package/dist/src/client/index.js.map +1 -0
  23. package/dist/src/client/transport.d.ts +12 -0
  24. package/dist/src/client/transport.d.ts.map +1 -0
  25. package/dist/src/client/transport.js +347 -0
  26. package/dist/src/client/transport.js.map +1 -0
  27. package/dist/src/client/types.d.ts +117 -0
  28. package/dist/src/client/types.d.ts.map +1 -0
  29. package/dist/src/client/types.js +10 -0
  30. package/dist/src/client/types.js.map +1 -0
  31. package/dist/src/client/validation.d.ts +3 -0
  32. package/dist/src/client/validation.d.ts.map +1 -0
  33. package/dist/src/client/validation.js +25 -0
  34. package/dist/src/client/validation.js.map +1 -0
  35. package/dist/src/compaction.d.ts +27 -0
  36. package/dist/src/compaction.d.ts.map +1 -0
  37. package/dist/src/compaction.js +155 -0
  38. package/dist/src/compaction.js.map +1 -0
  39. package/dist/src/content-upload.d.ts +59 -0
  40. package/dist/src/content-upload.d.ts.map +1 -0
  41. package/dist/src/content-upload.js +133 -0
  42. package/dist/src/content-upload.js.map +1 -0
  43. package/dist/src/draft/generated.d.ts +3111 -0
  44. package/dist/src/draft/generated.d.ts.map +1 -0
  45. package/dist/src/draft/generated.js +15623 -0
  46. package/dist/src/draft/generated.js.map +1 -0
  47. package/dist/src/draft/index.d.ts +34 -0
  48. package/dist/src/draft/index.d.ts.map +1 -0
  49. package/dist/src/draft/index.js +131 -0
  50. package/dist/src/draft/index.js.map +1 -0
  51. package/dist/src/draft/schemas.d.ts +8920 -0
  52. package/dist/src/draft/schemas.d.ts.map +1 -0
  53. package/dist/src/draft/schemas.js +9257 -0
  54. package/dist/src/draft/schemas.js.map +1 -0
  55. package/dist/src/draft-atomic.d.ts +18 -0
  56. package/dist/src/draft-atomic.d.ts.map +1 -0
  57. package/dist/src/draft-atomic.js +102 -0
  58. package/dist/src/draft-atomic.js.map +1 -0
  59. package/dist/src/draft-runtime.d.ts +89 -0
  60. package/dist/src/draft-runtime.d.ts.map +1 -0
  61. package/dist/src/draft-runtime.js +240 -0
  62. package/dist/src/draft-runtime.js.map +1 -0
  63. package/dist/src/elicitation.d.ts +18 -0
  64. package/dist/src/elicitation.d.ts.map +1 -0
  65. package/dist/src/elicitation.js +191 -0
  66. package/dist/src/elicitation.js.map +1 -0
  67. package/dist/src/errors.d.ts +7 -0
  68. package/dist/src/errors.d.ts.map +1 -0
  69. package/dist/src/errors.js +15 -0
  70. package/dist/src/errors.js.map +1 -0
  71. package/dist/src/framing.d.ts +7 -0
  72. package/dist/src/framing.d.ts.map +1 -0
  73. package/dist/src/framing.js +39 -0
  74. package/dist/src/framing.js.map +1 -0
  75. package/dist/src/generated.d.ts +304 -0
  76. package/dist/src/generated.d.ts.map +1 -0
  77. package/dist/src/generated.js +1885 -0
  78. package/dist/src/generated.js.map +1 -0
  79. package/dist/src/hook-runner.d.ts +11 -0
  80. package/dist/src/hook-runner.d.ts.map +1 -0
  81. package/dist/src/hook-runner.js +223 -0
  82. package/dist/src/hook-runner.js.map +1 -0
  83. package/dist/src/index.d.ts +8 -0
  84. package/dist/src/index.d.ts.map +1 -0
  85. package/dist/src/index.js +8 -0
  86. package/dist/src/index.js.map +1 -0
  87. package/dist/src/observation.d.ts +26 -0
  88. package/dist/src/observation.d.ts.map +1 -0
  89. package/dist/src/observation.js +28 -0
  90. package/dist/src/observation.js.map +1 -0
  91. package/dist/src/server/attachments.d.ts +29 -0
  92. package/dist/src/server/attachments.d.ts.map +1 -0
  93. package/dist/src/server/attachments.js +112 -0
  94. package/dist/src/server/attachments.js.map +1 -0
  95. package/dist/src/server/hooks.d.ts +20 -0
  96. package/dist/src/server/hooks.d.ts.map +1 -0
  97. package/dist/src/server/hooks.js +131 -0
  98. package/dist/src/server/hooks.js.map +1 -0
  99. package/dist/src/server/index.d.ts +9 -0
  100. package/dist/src/server/index.d.ts.map +1 -0
  101. package/dist/src/server/index.js +5 -0
  102. package/dist/src/server/index.js.map +1 -0
  103. package/dist/src/server/stdio.d.ts +15 -0
  104. package/dist/src/server/stdio.d.ts.map +1 -0
  105. package/dist/src/server/stdio.js +69 -0
  106. package/dist/src/server/stdio.js.map +1 -0
  107. package/dist/src/transport.d.ts +7 -0
  108. package/dist/src/transport.d.ts.map +1 -0
  109. package/dist/src/transport.js +195 -0
  110. package/dist/src/transport.js.map +1 -0
  111. package/dist/src/types.d.ts +116 -0
  112. package/dist/src/types.d.ts.map +1 -0
  113. package/dist/src/types.js +14 -0
  114. package/dist/src/types.js.map +1 -0
  115. package/dist/src/validation.d.ts +7 -0
  116. package/dist/src/validation.d.ts.map +1 -0
  117. package/dist/src/validation.js +209 -0
  118. package/dist/src/validation.js.map +1 -0
  119. package/package.json +62 -4
  120. package/src/client/auth.ts +1076 -0
  121. package/src/client/composition.ts +717 -0
  122. package/src/client/content.ts +522 -0
  123. package/src/client/hooks.ts +1477 -0
  124. package/src/client/index.ts +30 -0
  125. package/src/client/transport.ts +408 -0
  126. package/src/client/types.ts +173 -0
  127. package/src/client/validation.ts +24 -0
  128. package/src/compaction.ts +196 -0
  129. package/src/content-upload.ts +206 -0
  130. package/src/draft/README.md +16 -0
  131. package/src/draft/ahp-codegen.lock.json +131 -0
  132. package/src/draft/generated.ts +19449 -0
  133. package/src/draft/index.ts +251 -0
  134. package/src/draft/schemas.ts +9453 -0
  135. package/src/draft-atomic.ts +119 -0
  136. package/src/draft-runtime.ts +363 -0
  137. package/src/elicitation.ts +254 -0
  138. package/src/errors.ts +30 -0
  139. package/src/framing.ts +56 -0
  140. package/src/generated.ts +2408 -0
  141. package/src/hook-runner.ts +310 -0
  142. package/src/index.ts +8 -0
  143. package/src/observation.ts +65 -0
  144. package/src/server/attachments.ts +128 -0
  145. package/src/server/hooks.ts +206 -0
  146. package/src/server/index.ts +23 -0
  147. package/src/server/node-crypto.d.ts +7 -0
  148. package/src/server/stdio.ts +81 -0
  149. package/src/transport.ts +273 -0
  150. package/src/types.ts +124 -0
  151. package/src/validation.ts +372 -0
@@ -0,0 +1,1084 @@
1
+ import { toEventInput, contentSlots, CapabilityBuilder, Permission, } from "../draft/generated.js";
2
+ import { HookOperationalError } from "../errors.js";
3
+ import { randomUUID } from "node:crypto";
4
+ import { validateCapabilities, validateInterceptRequest, validateInterceptResponse, } from "../draft/index.js";
5
+ import { BackendTransport } from "./transport.js";
6
+ import { ContentManager, ContentSource } from "./content.js";
7
+ import { composeResponseAsync, normalizeEffects } from "./composition.js";
8
+ import { auth } from "./auth.js";
9
+ import { validateWire } from "./validation.js";
10
+ import { ConfigurationError, } from "./types.js";
11
+ const interceptable = new Set([
12
+ "tool.before",
13
+ "tool.after",
14
+ "session.start",
15
+ "config.change.before",
16
+ "turn.start",
17
+ "turn.finish.before",
18
+ "model.request.before",
19
+ "model.response.after",
20
+ "model.switch.before",
21
+ "tool.permission.request",
22
+ "tool.batch.after",
23
+ "context.compact.before",
24
+ "context.compact.after",
25
+ "task.change.before",
26
+ "user.elicitation.request",
27
+ "user.elicitation.result",
28
+ "user.message.inbound",
29
+ "user.message.outbound",
30
+ "workspace.change.before",
31
+ ]);
32
+ const eventTypes = [
33
+ "tool.before",
34
+ "tool.after",
35
+ "session.start",
36
+ "session.end",
37
+ "config.change.before",
38
+ "config.change.after",
39
+ "turn.start",
40
+ "turn.finish.before",
41
+ "turn.end",
42
+ "turn.progress",
43
+ "model.request.before",
44
+ "model.response.after",
45
+ "model.error",
46
+ "model.switch.before",
47
+ "model.switch.after",
48
+ "tool.permission.request",
49
+ "tool.permission.resolved",
50
+ "tool.progress",
51
+ "tool.batch.after",
52
+ "context.compact.before",
53
+ "context.compact.after",
54
+ "task.change.before",
55
+ "task.change.after",
56
+ "user.attention",
57
+ "user.elicitation.request",
58
+ "user.elicitation.result",
59
+ "user.message.inbound",
60
+ "user.message.outbound",
61
+ "workspace.change.before",
62
+ "workspace.change.after",
63
+ "file.changed",
64
+ "hook.failure",
65
+ ];
66
+ const matches = (selector, type) => selector === "*" ||
67
+ selector === type ||
68
+ (selector.endsWith(".*") && selector.slice(0, -2) === type.split(".")[0]);
69
+ /** Configuration-driven, harness-facing AHP client. No host operation is executed. */
70
+ export class Hooks {
71
+ initialized;
72
+ lifetime = new AbortController();
73
+ options;
74
+ routes = [];
75
+ managers = new Set();
76
+ pending = new Set();
77
+ provider;
78
+ authContexts = new Map();
79
+ manifest;
80
+ closed;
81
+ elicitations = new Map();
82
+ elicitationBytes = 0;
83
+ // Tokens exist only for active calls, including preparation before any bytes
84
+ // can be retained. Retirement invalidates those calls without tombstones.
85
+ activeElicitations = new Set();
86
+ constructor(config, options) {
87
+ this.options = {
88
+ ...options,
89
+ capabilities: structuredClone(Array.isArray(options.capabilities?.events)
90
+ ? options.capabilities
91
+ : Object.fromEntries(Object.entries(options.capabilities ?? {}).map(([event, grant]) => [
92
+ event,
93
+ grant instanceof CapabilityBuilder ? grant.build() : grant,
94
+ ]))),
95
+ };
96
+ this.provider = options.auth ?? auth();
97
+ // No credentials or processes are acquired until a matching boundary needs them.
98
+ this.initialized = Promise.resolve().then(() => this.initialize(config));
99
+ // An optional initialized wait must not cause an unhandled rejection.
100
+ void this.initialized.catch(() => { });
101
+ }
102
+ initialize(config) {
103
+ const issues = validateWire("registration", config).map((path) => ({
104
+ path,
105
+ code: "INVALID_REGISTRATION",
106
+ }));
107
+ for (const key of ["maxContentBytes", "observationTimeoutMs"]) {
108
+ const value = this.options[key];
109
+ if (value !== undefined &&
110
+ (!Number.isSafeInteger(value) ||
111
+ value <= 0 ||
112
+ (key === "observationTimeoutMs" && value > 2147483647)))
113
+ issues.push({ path: key, code: "INVALID_LIMIT" });
114
+ }
115
+ if (!this.options.source || typeof this.options.source !== "string")
116
+ issues.push({ path: "source", code: "INVALID_SOURCE" });
117
+ if (issues.length)
118
+ throw new ConfigurationError(issues);
119
+ const registration = structuredClone(config);
120
+ const c = this.options.capabilities;
121
+ if (!c || typeof c !== "object" || Array.isArray(c))
122
+ throw new ConfigurationError([
123
+ { path: "capabilities", code: "INVALID_CAPABILITIES" },
124
+ ]);
125
+ this.manifest = Array.isArray(c.events)
126
+ ? c
127
+ : {
128
+ authentication: ["bearer", "oauth"],
129
+ contentCategories: [
130
+ "text",
131
+ "images",
132
+ "audio",
133
+ "video",
134
+ "files",
135
+ "reasoning",
136
+ ],
137
+ correlationIdentityFields: [
138
+ "session.id",
139
+ "turn.id",
140
+ "call.id",
141
+ "parentEventId",
142
+ ],
143
+ events: Object.entries(c).map(([event, grant]) => {
144
+ const explicit = Object.hasOwn(grant ?? {}, "modes");
145
+ return {
146
+ event,
147
+ modes: explicit ? grant.modes : ["intercept"],
148
+ ...(explicit
149
+ ? grant.capabilities === undefined
150
+ ? {}
151
+ : { capabilities: grant.capabilities }
152
+ : { capabilities: grant }),
153
+ };
154
+ }),
155
+ gaps: [],
156
+ limits: {
157
+ maxUploadBytes: this.options.maxContentBytes ?? 64 * 1024 * 1024,
158
+ },
159
+ managedPolicy: {
160
+ disableable: false,
161
+ scopes: ["user", "project", "managed"],
162
+ },
163
+ toolPaths: ["*"],
164
+ transports: ["stdio", "http"],
165
+ };
166
+ const manifestErrors = validateWire("capabilities-response", {
167
+ jsonrpc: "2.0",
168
+ id: "init",
169
+ result: { protocolVersion: "draft", manifest: this.manifest },
170
+ });
171
+ for (const path of manifestErrors)
172
+ issues.push({
173
+ path: `capabilities${path}`,
174
+ code: "INVALID_CAPABILITIES",
175
+ });
176
+ for (const item of this.manifest.events) {
177
+ if (item.capabilities && !validateCapabilities(item.capabilities).ok)
178
+ issues.push({
179
+ path: `capabilities.${item.event}`,
180
+ code: "INVALID_CAPABILITIES",
181
+ });
182
+ if (item.modes.includes("intercept") &&
183
+ !interceptable.has(item.event))
184
+ issues.push({
185
+ path: `capabilities.${item.event}`,
186
+ code: "NOT_INTERCEPTABLE",
187
+ });
188
+ }
189
+ const ids = new Set();
190
+ for (const backend of registration.hooks) {
191
+ if (ids.has(backend.id))
192
+ issues.push({ path: backend.id, code: "DUPLICATE_BACKEND" });
193
+ ids.add(backend.id);
194
+ if (backend.transport.type === "http") {
195
+ try {
196
+ secureEndpoint(String(backend.transport.url));
197
+ }
198
+ catch {
199
+ issues.push({ path: backend.id, code: "UNSAFE_ENDPOINT" });
200
+ }
201
+ }
202
+ if (!["stdio", "http"].includes(backend.transport.type))
203
+ issues.push({ path: backend.id, code: "UNSUPPORTED_TRANSPORT" });
204
+ for (const [index, raw] of backend.subscriptions.entries()) {
205
+ const s = raw;
206
+ if (s.scope === "managed" && s.disableable !== false)
207
+ issues.push({
208
+ path: `${backend.id}.${index}`,
209
+ code: "MANDATORY_SCOPE_DISABLEABLE",
210
+ });
211
+ if (s.scope && !this.manifest.managedPolicy.scopes.includes(s.scope))
212
+ issues.push({
213
+ path: `${backend.id}.${index}`,
214
+ code: "UNSUPPORTED_SCOPE",
215
+ });
216
+ for (const selector of s.events) {
217
+ if (!eventTypes.some((e) => matches(selector, e)))
218
+ issues.push({
219
+ path: `${backend.id}.${index}`,
220
+ code: "UNSUPPORTED_EVENT",
221
+ });
222
+ if (!selector.includes("*") &&
223
+ !this.manifest.events.some((e) => e.event === selector && e.modes.includes(s.mode)))
224
+ issues.push({
225
+ path: `${backend.id}.${index}`,
226
+ code: s.mode === "intercept" ? "NOT_INTERCEPTABLE" : "NOT_OBSERVABLE",
227
+ });
228
+ }
229
+ }
230
+ }
231
+ if (issues.length)
232
+ throw new ConfigurationError(issues);
233
+ for (const backend of registration.hooks) {
234
+ const transport = new BackendTransport(backend, (url, init) => this.fetchAuthenticated(url, init, backend.authentication, "event", backend.id));
235
+ backend.subscriptions.forEach((subscription, index) => this.routes.push({
236
+ backend,
237
+ subscription: subscription,
238
+ index,
239
+ transport,
240
+ }));
241
+ }
242
+ }
243
+ // Each endpoint binding has its own provider context; upload auth never inherits
244
+ // event auth. Contexts belong to this Hooks instance, even with a shared provider.
245
+ async fetchAuthenticated(url, init, authentication, purpose, backendId) {
246
+ secureEndpoint(url);
247
+ const signal = init.signal ?? this.lifetime.signal;
248
+ signal.throwIfAborted();
249
+ const key = JSON.stringify([
250
+ backendId,
251
+ purpose,
252
+ url,
253
+ authentication ?? null,
254
+ ]);
255
+ let context = this.authContexts.get(key);
256
+ if (!context) {
257
+ context = {
258
+ url,
259
+ backendId,
260
+ purpose,
261
+ ...(authentication ? { authentication } : {}),
262
+ cache: new Map(),
263
+ };
264
+ this.authContexts.set(key, context);
265
+ }
266
+ // Challenge state belongs to this delivery attempt, never another caller.
267
+ context = { ...context };
268
+ for (let attempt = 0; attempt < 2; attempt++) {
269
+ const credential = await raceAbort(Promise.resolve("credential" in this.provider
270
+ ? this.provider.credential({ ...context, signal })
271
+ : this.provider.authenticate({ ...context, signal }, {})), signal);
272
+ if (!credential &&
273
+ (authentication !== undefined || context.challenged === true))
274
+ throw new Error("Authentication credential required");
275
+ const headers = new Headers(init.headers);
276
+ headers.delete("authorization");
277
+ if (credential) {
278
+ if (typeof credential.token !== "string" ||
279
+ !credential.token ||
280
+ /[\s\x00-\x1f\x7f]/.test(credential.token) ||
281
+ ("credential" in this.provider &&
282
+ (!("type" in credential) || credential.type !== "bearer")) ||
283
+ ("expiresAt" in credential &&
284
+ credential.expiresAt !== undefined &&
285
+ (!Number.isFinite(credential.expiresAt) ||
286
+ credential.expiresAt <= Date.now())))
287
+ throw new Error("Invalid authentication credential");
288
+ headers.set("authorization", `Bearer ${credential.token}`);
289
+ }
290
+ const response = await raceAbort((this.options.fetch ?? fetch)(url, {
291
+ ...init,
292
+ headers,
293
+ redirect: "error",
294
+ signal,
295
+ }).then((response) => {
296
+ if (signal.aborted) {
297
+ void response.body?.cancel().catch(() => { });
298
+ signal.throwIfAborted();
299
+ }
300
+ return response;
301
+ }), signal);
302
+ if (response.status === 401 && "credential" in this.provider) {
303
+ try {
304
+ await raceAbort(Promise.resolve(this.provider.challenge({
305
+ ...context,
306
+ signal,
307
+ response,
308
+ credential,
309
+ })), signal);
310
+ }
311
+ catch (error) {
312
+ void response.body?.cancel().catch(() => { });
313
+ throw error;
314
+ }
315
+ }
316
+ if (response.status !== 401 || attempt === 1) {
317
+ delete context.challenge;
318
+ delete context.challenged;
319
+ return response;
320
+ }
321
+ context.challenged = true;
322
+ context.challenge = response.headers.get("www-authenticate") ?? undefined;
323
+ void response.body?.cancel().catch(() => { });
324
+ }
325
+ throw new Error("Authentication failed");
326
+ }
327
+ /** Generic typed boundary dispatch; named methods below cover the complete catalogue. */
328
+ async dispatch(type, input, options = {}) {
329
+ const manager = new ContentManager({
330
+ // Invalid configuration still transfers unused-source cleanup ownership;
331
+ // initialize rejects it before any preparation can use this fallback.
332
+ maxSnapshotBytes: Number.isSafeInteger(this.options.maxContentBytes) &&
333
+ this.options.maxContentBytes > 0
334
+ ? this.options.maxContentBytes
335
+ : 64 * 1024 * 1024,
336
+ allowLoopback: true,
337
+ });
338
+ manager.own(input);
339
+ manager.own(options.contentSources);
340
+ this.managers.add(manager);
341
+ const signal = combineSignals(this.lifetime.signal, options.signal);
342
+ const exchange = type === "user.elicitation.request"
343
+ ? {
344
+ id: input.id ?? randomUUID(),
345
+ sessionId: input.session?.id,
346
+ retired: false,
347
+ }
348
+ : undefined;
349
+ if (exchange)
350
+ this.activeElicitations.add(exchange);
351
+ let completed = false;
352
+ const work = (async () => {
353
+ // Calls after close are misuse. Active calls return interruption evidence.
354
+ this.lifetime.signal.throwIfAborted();
355
+ await this.initialized;
356
+ return this.run(type, input, options, signal, manager, exchange);
357
+ })();
358
+ this.pending.add(work);
359
+ try {
360
+ const result = await work;
361
+ completed = true;
362
+ return result;
363
+ }
364
+ finally {
365
+ // Terminal exchanges retire even when validation or cancellation bypasses run.
366
+ if (type === "user.elicitation.result")
367
+ this.forgetElicitation(input.parentEventId);
368
+ if (type === "user.elicitation.request" &&
369
+ (!completed || signal.aborted) &&
370
+ input.id)
371
+ this.forgetElicitation(input.id);
372
+ if (type === "session.end")
373
+ this.retireSessionElicitations(input.session?.id);
374
+ if (exchange)
375
+ this.activeElicitations.delete(exchange);
376
+ await manager.close();
377
+ this.managers.delete(manager);
378
+ this.pending.delete(work);
379
+ }
380
+ }
381
+ async run(type, input, options, signal, manager, exchange) {
382
+ let event = cloneInput({
383
+ ...input,
384
+ type,
385
+ source: this.options.source,
386
+ id: exchange?.id ?? input.id ?? randomUUID(),
387
+ time: input.time ?? new Date().toISOString(),
388
+ });
389
+ bindContentSources(type, event, options.contentSources);
390
+ if (type === "session.start")
391
+ event.manifest = structuredClone(this.manifest);
392
+ const requestId = event.id;
393
+ // Project actual settled state, never replay cumulative effects: that would
394
+ // duplicate injections/instructions and lose candidate invalidation.
395
+ const snapshotState = (value) => {
396
+ const snapshot = structuredClone(value ?? { candidate: null, permission: "none" });
397
+ if (!Object.values(Permission).includes(snapshot.permission))
398
+ throw new ConfigurationError([
399
+ { path: "initialState.permission", code: "INVALID_BOUNDARY_STATE" },
400
+ ]);
401
+ const freeze = (value) => {
402
+ if (value !== null && typeof value === "object") {
403
+ for (const child of Object.values(value))
404
+ freeze(child);
405
+ Object.freeze(value);
406
+ }
407
+ };
408
+ freeze(snapshot);
409
+ return snapshot;
410
+ };
411
+ if (signal.aborted)
412
+ return {
413
+ event: await resultEvent(event, manager),
414
+ response: {
415
+ jsonrpc: "2.0",
416
+ id: requestId,
417
+ result: { protocolVersion: "draft", effects: [] },
418
+ },
419
+ state: snapshotState(options.initialState),
420
+ get permission() {
421
+ return this.state.permission;
422
+ },
423
+ get input() {
424
+ return this.event.tool?.input;
425
+ },
426
+ errors: [],
427
+ diagnostics: [],
428
+ observations: Promise.resolve([]),
429
+ interrupted: true,
430
+ };
431
+ const localSources = new Set();
432
+ const originalRequest = this.elicitationFor(event);
433
+ manager.own(originalRequest);
434
+ let effects = [];
435
+ let state = structuredClone(options.initialState);
436
+ // A supplied snapshot is input to the first receiver, not a newly accepted
437
+ // result in this chain. Composition settles terminal state after acceptance.
438
+ let shortCircuit = false;
439
+ let interrupted = false;
440
+ const errors = [];
441
+ const advertised = this.manifest.events.find((e) => e.event === type);
442
+ if (!advertised)
443
+ throw new ConfigurationError([
444
+ { path: type, code: "UNADVERTISED_EVENT" },
445
+ ]);
446
+ const staticCaps = advertised.capabilities ?? { effects: [] };
447
+ const caps = structuredClone(options.capabilities ?? staticCaps);
448
+ // Reject widening before deriving occurrence counters or clamping budgets.
449
+ if (!validateCapabilities(caps).ok || !isNarrower(caps, staticCaps))
450
+ throw new ConfigurationError([
451
+ { path: type, code: "INVALID_CAPABILITY_NARROWING" },
452
+ ]);
453
+ if (type === "turn.finish.before" && caps.flow) {
454
+ caps.flow.continuationCount = event.continuationCount;
455
+ if (caps.flow.maxContinuations !== undefined)
456
+ caps.flow.remainingContinuations = Math.max(0, Math.min(caps.flow.remainingContinuations ?? 0, caps.flow.maxContinuations - event.continuationCount));
457
+ }
458
+ if (!validateCapabilities(caps).ok ||
459
+ !isNarrower(caps, advertised.capabilities ?? { effects: [] }))
460
+ throw new ConfigurationError([
461
+ { path: type, code: "INVALID_CAPABILITY_NARROWING" },
462
+ ]);
463
+ const called = new Set();
464
+ const matching = this.routes.filter((r) => r.subscription.events.some((s) => matches(s, type)) &&
465
+ advertised.modes.includes(r.subscription.mode));
466
+ try {
467
+ // Validate a metadata-only view before any delivery (this does not consume bytes).
468
+ const metadata = await manager.prepare(event, { default: "metadata" }, undefined, () => Promise.reject(new Error("Unexpected upload")), signal);
469
+ const invalid = validateWire("observe-notification", {
470
+ jsonrpc: "2.0",
471
+ method: "hooks/observe",
472
+ params: { protocolVersion: "draft", event: metadata },
473
+ });
474
+ if (invalid.length)
475
+ throw new ConfigurationError(invalid.map((path) => ({ path, code: "INVALID_EVENT" })));
476
+ const invalidState = state === undefined
477
+ ? []
478
+ : validateWire("intercept-request", {
479
+ jsonrpc: "2.0",
480
+ id: requestId,
481
+ method: "hooks/intercept",
482
+ params: {
483
+ protocolVersion: "draft",
484
+ event: metadata,
485
+ capabilities: caps,
486
+ ...(state !== undefined ? { state } : {}),
487
+ },
488
+ });
489
+ if (invalidState.length)
490
+ throw new ConfigurationError(invalidState.map((path) => ({
491
+ path,
492
+ code: "INVALID_BOUNDARY_STATE",
493
+ })));
494
+ for (const route of matching) {
495
+ if (route.subscription.mode !== "intercept" || shortCircuit)
496
+ continue;
497
+ if (signal.aborted) {
498
+ interrupted = true;
499
+ break;
500
+ }
501
+ called.add(route);
502
+ const s = route.subscription;
503
+ let phase = "preparation";
504
+ let deadline;
505
+ let received = false;
506
+ let budget;
507
+ try {
508
+ const projected = await raceAbort(manager.prepare(projectNative(event, s.includeNative), s.content, s.upload, (url, init, upload) => this.fetchAuthenticated(url, init, upload.auth, "upload", route.backend.id), signal), signal);
509
+ if (type === "user.elicitation.request" &&
510
+ projected.elicitation?.request?.selection === "body" &&
511
+ event.elicitation?.request?.body instanceof ReadableStream) {
512
+ const bytes = await manager.readBody(event.elicitation.request.body, signal);
513
+ this.rememberElicitation(event, bytes, exchange);
514
+ }
515
+ const request = {
516
+ jsonrpc: "2.0",
517
+ id: requestId,
518
+ method: "hooks/intercept",
519
+ params: {
520
+ protocolVersion: "draft",
521
+ event: projected,
522
+ capabilities: caps,
523
+ ...(state ? { state } : {}),
524
+ },
525
+ };
526
+ const valid = validateInterceptRequest(request);
527
+ if (!valid.ok)
528
+ throw new Error("Invalid interception request");
529
+ phase = "interception";
530
+ budget = deadlineAfter(s.timeoutMs, signal);
531
+ deadline = budget.signal;
532
+ const reply = await route.transport.request(request, deadline);
533
+ received = true;
534
+ budget.check();
535
+ if (reply && typeof reply === "object" && "error" in reply)
536
+ throw new HookOperationalError("JSON_RPC_ERROR", "Backend returned a JSON-RPC error");
537
+ const decoded = validateInterceptResponse(reply);
538
+ if (!decoded.ok || decoded.value.id !== requestId)
539
+ throw new Error("Invalid interception response");
540
+ // Elicitation terminals are mutually exclusive. Check the raw reply
541
+ // before generic composition normalizes return + deny to denial.
542
+ if (type === "user.elicitation.request" &&
543
+ decoded.value.result.effects.some((effect) => effect.type === "return") &&
544
+ decoded.value.result.effects.some((effect) => effect.type === "deny"))
545
+ throw new Error("Conflicting elicitation terminal effects");
546
+ if (type === "user.elicitation.request" &&
547
+ decoded.value.result.effects.some((effect) => effect.type === "return" || effect.type === "deny") &&
548
+ (projected.elicitation?.request?.selection !== "body" ||
549
+ !projected.elicitation.request.body ||
550
+ projected.elicitation.request.gap !== undefined))
551
+ throw new Error("Elicitation terminals require selected request body");
552
+ const staged = await raceAbort(composeResponseAsync(event, effects, decoded.value, caps, {
553
+ readContent: (body) => manager.readBody(body, deadline),
554
+ ownContent: (body) => {
555
+ manager.own(body);
556
+ localSources.add(body);
557
+ },
558
+ selectedEvent: projected,
559
+ state,
560
+ ...(originalRequest
561
+ ? { elicitationRequest: originalRequest }
562
+ : {}),
563
+ }), deadline);
564
+ budget.check();
565
+ event = staged.event;
566
+ effects = staged.effects;
567
+ state = staged.state;
568
+ shortCircuit = staged.shortCircuit;
569
+ }
570
+ catch (cause) {
571
+ interrupted = signal.aborted;
572
+ const syntheticDenial = !interrupted && s.failurePolicy === "fail-closed";
573
+ const error = {
574
+ backendId: route.backend.id,
575
+ subscriptionIndex: route.index,
576
+ phase,
577
+ code: interrupted
578
+ ? cancellationCode(signal)
579
+ : deadline?.aborted
580
+ ? "DEADLINE_EXCEEDED"
581
+ : phase === "preparation"
582
+ ? "PREPARATION_FAILED"
583
+ : "DELIVERY_FAILED",
584
+ failurePolicy: s.failurePolicy,
585
+ syntheticDenial,
586
+ };
587
+ diagnosticCauses.set(error, classifyDiagnostic(cause, signal.aborted ? signal : deadline, phase === "preparation", received));
588
+ errors.push(error);
589
+ if (interrupted)
590
+ break;
591
+ if (syntheticDenial) {
592
+ const denial = {
593
+ type: "deny",
594
+ reason: "Required policy backend unavailable.",
595
+ };
596
+ const settled = normalizeEffects([denial], state);
597
+ effects = normalizeEffects([...effects, denial]).effects;
598
+ state = settled.state;
599
+ shortCircuit = settled.shortCircuit;
600
+ }
601
+ }
602
+ finally {
603
+ budget?.dispose();
604
+ }
605
+ }
606
+ interrupted ||= signal.aborted;
607
+ if (interrupted)
608
+ effects = effects.filter((e) => e.type !== "allow" &&
609
+ e.type !== "return" &&
610
+ !(e.type === "flow" && e.operation === "continue"));
611
+ // Observation deliveries are owned by this call, but cannot change its decision.
612
+ const observations = matching.filter((r) => advertised.modes.includes("observe") &&
613
+ (r.subscription.mode === "observe" ||
614
+ ((shortCircuit || interrupted) && !called.has(r))));
615
+ let observed = Promise.resolve([]);
616
+ // Cancellation stops new work, including settlement notifications. Each
617
+ // observer budget can shorten, but never extend, the operation signal.
618
+ if (!signal.aborted && observations.length) {
619
+ const snapshot = cloneInput(event);
620
+ const jobs = observations.map((route) => this.observe(route, snapshot, manager, signal));
621
+ observed = Promise.all(jobs).then((results) => results.filter((result) => result !== undefined));
622
+ await observed;
623
+ }
624
+ interrupted ||= signal.aborted;
625
+ if (type === "user.elicitation.result")
626
+ this.forgetElicitation(event.parentEventId);
627
+ if (type === "user.elicitation.request" &&
628
+ (interrupted ||
629
+ errors.length > 0 ||
630
+ (await observed).length > 0 ||
631
+ effects.some((e) => e.type === "deny")))
632
+ this.forgetElicitation(event.id);
633
+ if (type === "session.end")
634
+ this.retireSessionElicitations(event.session?.id);
635
+ const response = {
636
+ jsonrpc: "2.0",
637
+ id: requestId,
638
+ result: { protocolVersion: "draft", effects },
639
+ };
640
+ return {
641
+ event: await resultEvent(event, manager, localSources),
642
+ response,
643
+ state: snapshotState(state),
644
+ get permission() {
645
+ return this.state.permission;
646
+ },
647
+ get input() {
648
+ return this.event.tool?.input;
649
+ },
650
+ errors,
651
+ diagnostics: [...errors, ...(await observed)].map(deliveryDiagnostic),
652
+ observations: observed,
653
+ interrupted,
654
+ };
655
+ }
656
+ catch (error) {
657
+ if (type === "user.elicitation.request")
658
+ this.forgetElicitation(requestId);
659
+ await manager.close();
660
+ this.managers.delete(manager);
661
+ throw error;
662
+ }
663
+ }
664
+ async observe(route, event, manager, parentSignal) {
665
+ const budget = deadlineAfter(this.options.observationTimeoutMs ?? 15000, parentSignal);
666
+ const signal = budget.signal;
667
+ let preparing = true;
668
+ try {
669
+ const s = route.subscription;
670
+ const projected = await raceAbort(manager.prepare(projectNative(event, s.includeNative), s.content, s.upload, (url, init, upload) => this.fetchAuthenticated(url, init, upload.auth, "upload", route.backend.id), signal), signal);
671
+ preparing = false;
672
+ await route.transport.notify({
673
+ jsonrpc: "2.0",
674
+ method: "hooks/observe",
675
+ params: { protocolVersion: "draft", event: projected },
676
+ }, signal);
677
+ }
678
+ catch (cause) {
679
+ const error = {
680
+ backendId: route.backend.id,
681
+ subscriptionIndex: route.index,
682
+ phase: "observation",
683
+ code: parentSignal.aborted
684
+ ? cancellationCode(parentSignal)
685
+ : signal.aborted
686
+ ? "DEADLINE_EXCEEDED"
687
+ : "DELIVERY_FAILED",
688
+ syntheticDenial: false,
689
+ };
690
+ diagnosticCauses.set(error, classifyDiagnostic(cause, signal, preparing, false));
691
+ return error;
692
+ }
693
+ finally {
694
+ budget.dispose();
695
+ }
696
+ }
697
+ rememberElicitation(event, bytes, exchange) {
698
+ if (exchange.retired || this.lifetime.signal.aborted)
699
+ return;
700
+ if (this.elicitations.has(event.id))
701
+ return;
702
+ const limit = this.options.maxContentBytes ?? 64 * 1024 * 1024;
703
+ if (bytes.byteLength > limit)
704
+ throw new Error("Elicitation request exceeds retention budget");
705
+ if (this.elicitationBytes + bytes.byteLength > limit ||
706
+ this.elicitations.size >= 128)
707
+ throw new Error("Active elicitation exchanges exceed retention budget");
708
+ // Pending exchanges own only correlation facts and selected request bytes.
709
+ const stored = {
710
+ type: event.type,
711
+ id: event.id,
712
+ source: event.source,
713
+ session: event.session ? { id: event.session.id } : undefined,
714
+ elicitation: {
715
+ mode: event.elicitation.mode,
716
+ server: event.elicitation.server,
717
+ request: { mediaType: event.elicitation.request.mediaType },
718
+ },
719
+ };
720
+ this.elicitations.set(event.id, { event: stored, bytes });
721
+ this.elicitationBytes += bytes.byteLength;
722
+ }
723
+ elicitationFor(event) {
724
+ if (event.type !== "user.elicitation.result")
725
+ return undefined;
726
+ const entry = this.elicitations.get(event.parentEventId);
727
+ if (!entry)
728
+ return undefined;
729
+ const copy = cloneInput(entry.event), bytes = entry.bytes.slice();
730
+ copy.elicitation.request.body = new ReadableStream({
731
+ start(c) {
732
+ c.enqueue(bytes);
733
+ c.close();
734
+ },
735
+ });
736
+ return copy;
737
+ }
738
+ /** End an abandoned host elicitation exchange without delivering a result. */
739
+ discardElicitation(requestEventId) {
740
+ this.forgetElicitation(requestEventId);
741
+ }
742
+ retireSessionElicitations(sessionId) {
743
+ for (const exchange of this.activeElicitations)
744
+ if (exchange.sessionId === sessionId)
745
+ exchange.retired = true;
746
+ for (const [id, entry] of this.elicitations)
747
+ if (entry.event.session?.id === sessionId)
748
+ this.forgetElicitation(id);
749
+ }
750
+ forgetElicitation(id) {
751
+ for (const exchange of this.activeElicitations)
752
+ if (exchange.id === id)
753
+ exchange.retired = true;
754
+ const entry = this.elicitations.get(id);
755
+ if (entry)
756
+ this.elicitationBytes -= entry.bytes.byteLength;
757
+ this.elicitations.delete(id);
758
+ }
759
+ /** Cancel pending work and release SDK-owned resources; injected stores remain owned by the host. */
760
+ close() {
761
+ if (!this.closed) {
762
+ for (const exchange of this.activeElicitations)
763
+ exchange.retired = true;
764
+ this.lifetime.abort(new Error("Hooks closed"));
765
+ this.closed = (async () => {
766
+ await this.initialized.catch(() => { });
767
+ await Promise.allSettled([...new Set(this.routes.map((r) => r.transport))].map((t) => t.close()));
768
+ await Promise.allSettled([...this.managers].map((m) => m.close()));
769
+ await Promise.allSettled([...this.pending]);
770
+ this.authContexts.clear();
771
+ this.elicitations.clear();
772
+ this.elicitationBytes = 0;
773
+ })();
774
+ }
775
+ return this.closed;
776
+ }
777
+ boundary(type, input, options) {
778
+ // Generated projection preserves owned source identities; dispatch clones
779
+ // host facts, owns cleanup, and validates the canonical request before I/O.
780
+ return this.dispatch(type, toEventInput(type, input), options);
781
+ }
782
+ toolBefore(input, options) {
783
+ return this.boundary("tool.before", input, options);
784
+ }
785
+ toolAfter(input, options) {
786
+ return this.boundary("tool.after", input, options);
787
+ }
788
+ sessionStart(input, options) {
789
+ return this.boundary("session.start", input, options);
790
+ }
791
+ sessionEnd(input, options) {
792
+ return this.boundary("session.end", input, options);
793
+ }
794
+ configChangeBefore(input, options) {
795
+ return this.boundary("config.change.before", input, options);
796
+ }
797
+ configChangeAfter(input, options) {
798
+ return this.boundary("config.change.after", input, options);
799
+ }
800
+ turnStart(input, options) {
801
+ return this.boundary("turn.start", input, options);
802
+ }
803
+ turnFinishBefore(input, options) {
804
+ return this.boundary("turn.finish.before", input, options);
805
+ }
806
+ turnEnd(input, options) {
807
+ return this.boundary("turn.end", input, options);
808
+ }
809
+ turnProgress(input, options) {
810
+ return this.boundary("turn.progress", input, options);
811
+ }
812
+ modelRequestBefore(input, options) {
813
+ return this.boundary("model.request.before", input, options);
814
+ }
815
+ modelResponseAfter(input, options) {
816
+ return this.boundary("model.response.after", input, options);
817
+ }
818
+ modelError(input, options) {
819
+ return this.boundary("model.error", input, options);
820
+ }
821
+ modelSwitchBefore(input, options) {
822
+ return this.boundary("model.switch.before", input, options);
823
+ }
824
+ modelSwitchAfter(input, options) {
825
+ return this.boundary("model.switch.after", input, options);
826
+ }
827
+ toolPermissionRequest(input, options) {
828
+ return this.boundary("tool.permission.request", input, options);
829
+ }
830
+ toolPermissionResolved(input, options) {
831
+ return this.boundary("tool.permission.resolved", input, options);
832
+ }
833
+ toolProgress(input, options) {
834
+ return this.boundary("tool.progress", input, options);
835
+ }
836
+ toolBatchAfter(input, options) {
837
+ return this.boundary("tool.batch.after", input, options);
838
+ }
839
+ contextCompactBefore(input, options) {
840
+ return this.boundary("context.compact.before", input, options);
841
+ }
842
+ contextCompactAfter(input, options) {
843
+ return this.boundary("context.compact.after", input, options);
844
+ }
845
+ taskChangeBefore(input, options) {
846
+ return this.boundary("task.change.before", input, options);
847
+ }
848
+ taskChangeAfter(input, options) {
849
+ return this.boundary("task.change.after", input, options);
850
+ }
851
+ userAttention(input, options) {
852
+ return this.boundary("user.attention", input, options);
853
+ }
854
+ userElicitationRequest(input, options) {
855
+ return this.boundary("user.elicitation.request", input, options);
856
+ }
857
+ userElicitationResult(input, options) {
858
+ return this.boundary("user.elicitation.result", input, options);
859
+ }
860
+ userMessageInbound(input, options) {
861
+ return this.boundary("user.message.inbound", input, options);
862
+ }
863
+ userMessageOutbound(input, options) {
864
+ return this.boundary("user.message.outbound", input, options);
865
+ }
866
+ workspaceChangeBefore(input, options) {
867
+ return this.boundary("workspace.change.before", input, options);
868
+ }
869
+ workspaceChangeAfter(input, options) {
870
+ return this.boundary("workspace.change.after", input, options);
871
+ }
872
+ fileChanged(input, options) {
873
+ return this.boundary("file.changed", input, options);
874
+ }
875
+ hookFailure(input, options) {
876
+ return this.boundary("hook.failure", input, options);
877
+ }
878
+ }
879
+ // Return independent copies of prepared or synthesized payloads. Never read an
880
+ // unselected producer stream solely to construct a result.
881
+ async function resultEvent(value, manager, localSources = new Set()) {
882
+ if (value instanceof ContentSource)
883
+ value = value.stream;
884
+ if (value instanceof ReadableStream) {
885
+ const bytes = localSources.has(value)
886
+ ? await manager.readBody(value)
887
+ : await manager.copySnapshot(value);
888
+ if (!bytes)
889
+ return undefined;
890
+ return new ReadableStream({
891
+ start(controller) {
892
+ controller.enqueue(bytes);
893
+ controller.close();
894
+ },
895
+ });
896
+ }
897
+ if (Array.isArray(value))
898
+ return Promise.all(value.map((child) => resultEvent(child, manager, localSources)));
899
+ if (value && typeof value === "object") {
900
+ const entries = await Promise.all(Object.entries(value).map(async ([key, child]) => [key, await resultEvent(child, manager, localSources)]));
901
+ const copy = Object.fromEntries(entries.filter(([, child]) => child !== undefined));
902
+ if ((value.body instanceof ReadableStream || value.body instanceof ContentSource) &&
903
+ !copy.body)
904
+ copy.selection = "metadata";
905
+ return copy;
906
+ }
907
+ return value;
908
+ }
909
+ function cloneInput(value) {
910
+ if (value instanceof ContentSource)
911
+ return value.stream;
912
+ if (value instanceof ReadableStream)
913
+ return value;
914
+ if (Array.isArray(value))
915
+ return value.map(cloneInput);
916
+ if (value && typeof value === "object")
917
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, cloneInput(v)]));
918
+ return value;
919
+ }
920
+ function projectNative(event, includeNative) {
921
+ const copy = cloneInput(event);
922
+ if (includeNative !== true)
923
+ delete copy.native;
924
+ return copy;
925
+ }
926
+ // Only known capability fields carry permission semantics. Extension fields at
927
+ // every level remain opaque; their values must not affect local narrowing.
928
+ const modifyOperations = { replace: true, merge: true };
929
+ const capabilitySemantics = {
930
+ effects: true,
931
+ elicitation: { form: {}, url: {} },
932
+ flow: {
933
+ operations: true,
934
+ continuationCount: true,
935
+ maxContinuations: true,
936
+ remainingContinuations: true,
937
+ },
938
+ inject: { context: { append: true, deliverAt: true } },
939
+ modify: Object.fromEntries([
940
+ "content",
941
+ "input",
942
+ "instructions",
943
+ "output",
944
+ "prompt",
945
+ "request",
946
+ "response",
947
+ "summary",
948
+ "workspace",
949
+ ].map((target) => [target, modifyOperations])),
950
+ };
951
+ function isNarrower(next, previous, semantics = capabilitySemantics) {
952
+ if (semantics === true) {
953
+ if (Array.isArray(next))
954
+ return Array.isArray(previous) && next.every((v) => previous.includes(v));
955
+ if (typeof next === "number" && typeof previous === "number")
956
+ return next <= previous;
957
+ return next === previous || next === false;
958
+ }
959
+ return Object.entries(next).every(([key, value]) => !Object.hasOwn(semantics, key) ||
960
+ (key === "continuationCount"
961
+ ? typeof value === "number" && value >= (previous?.[key] ?? 0)
962
+ : (semantics[key] === true || previous?.[key] !== undefined) &&
963
+ isNarrower(value, previous?.[key], semantics[key])));
964
+ }
965
+ function combineSignals(first, second) {
966
+ return second ? AbortSignal.any([first, second]) : first;
967
+ }
968
+ function raceAbort(promise, signal) {
969
+ return new Promise((resolve, reject) => {
970
+ const abort = () => reject(signal.reason);
971
+ signal.addEventListener("abort", abort, { once: true });
972
+ promise
973
+ .then(resolve, reject)
974
+ .finally(() => signal.removeEventListener("abort", abort));
975
+ // The operation already exists: own its rejection even if cancellation
976
+ // occurred synchronously while constructing it.
977
+ if (signal.aborted)
978
+ abort();
979
+ });
980
+ }
981
+ function secureEndpoint(value) {
982
+ const url = new URL(value);
983
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
984
+ if (url.username ||
985
+ url.password ||
986
+ url.hash ||
987
+ !(url.protocol === "https:" || (url.protocol === "http:" && loopback)))
988
+ throw new Error("Unsafe endpoint");
989
+ }
990
+ function deadlineAfter(ms, parent) {
991
+ const controller = new AbortController();
992
+ const expires = Date.now() + ms;
993
+ const timer = setTimeout(() => controller.abort(new DOMException("Hook deadline exceeded", "TimeoutError")), ms);
994
+ const signal = combineSignals(parent, controller.signal);
995
+ return {
996
+ signal,
997
+ dispose: () => clearTimeout(timer),
998
+ check: () => {
999
+ if (Date.now() >= expires)
1000
+ controller.abort(new DOMException("Hook deadline exceeded", "TimeoutError"));
1001
+ signal.throwIfAborted();
1002
+ },
1003
+ };
1004
+ }
1005
+ function cancellationCode(signal) {
1006
+ return signal.reason?.name === "TimeoutError"
1007
+ ? "DEADLINE_EXCEEDED"
1008
+ : "INTERRUPTED";
1009
+ }
1010
+ // Causes remain separate from delivery stage and never retain raw endpoint data.
1011
+ const diagnosticCauses = new WeakMap();
1012
+ function deliveryDiagnostic(error) {
1013
+ return { ...error, code: diagnosticCauses.get(error) ?? "transport" };
1014
+ }
1015
+ function classifyDiagnostic(cause, signal, preparing, received) {
1016
+ if (signal?.aborted)
1017
+ return signal.reason?.name === "TimeoutError"
1018
+ ? "deadline_exceeded"
1019
+ : "cancelled";
1020
+ if (cause instanceof Error && cause.name === "TimeoutError")
1021
+ return "deadline_exceeded";
1022
+ if (cause instanceof HookOperationalError) {
1023
+ if (cause.code === "JSON_RPC_ERROR")
1024
+ return "remote_rpc";
1025
+ if ([
1026
+ "MALFORMED_UTF8",
1027
+ "MALFORMED_JSON",
1028
+ "MALFORMED_JSON_RPC",
1029
+ "ID_MISMATCH",
1030
+ "INCOMPATIBLE_VERSION",
1031
+ "UNSUPPORTED_EVENT",
1032
+ "UNSUPPORTED_EFFECT",
1033
+ "MULTIPLE_EFFECTS",
1034
+ ].includes(cause.code))
1035
+ return "protocol_rejection";
1036
+ }
1037
+ if (preparing)
1038
+ return "preparation";
1039
+ return received ? "protocol_rejection" : "transport";
1040
+ }
1041
+ /** Only schema-derived named slots may receive out-of-band sources. */
1042
+ function bindContentSources(type, event, bindings) {
1043
+ const used = new Set();
1044
+ for (const binding of bindings ?? []) {
1045
+ const source = binding.source instanceof ContentSource
1046
+ ? binding.source.stream
1047
+ : binding.source;
1048
+ if (!(source instanceof ReadableStream))
1049
+ throw new TypeError("Expected an owned content source");
1050
+ const path = binding.path;
1051
+ const allowed = Object.values(contentSlots[type]).some((factory) => {
1052
+ const bind = factory;
1053
+ const expected = (factory.length === 2 ? bind(0, source) : bind(source))
1054
+ .path;
1055
+ return (path.length === expected.length &&
1056
+ expected.every((part, index) => typeof part === "number"
1057
+ ? Number.isSafeInteger(path[index]) && Number(path[index]) >= 0
1058
+ : path[index] === part));
1059
+ });
1060
+ const key = JSON.stringify(path);
1061
+ if (!allowed || used.has(key))
1062
+ throw new TypeError("Invalid or duplicate content source slot");
1063
+ used.add(key);
1064
+ let parent = event;
1065
+ for (const part of path.slice(0, -1)) {
1066
+ parent = parent?.[part];
1067
+ if (!parent || typeof parent !== "object")
1068
+ throw new TypeError("Missing content source descriptor");
1069
+ }
1070
+ const last = path[path.length - 1];
1071
+ const descriptor = parent[last];
1072
+ if (descriptor && typeof descriptor === "object" && "kind" in descriptor) {
1073
+ if (descriptor.body !== undefined)
1074
+ throw new TypeError("Content source conflicts with an existing body");
1075
+ descriptor.body = source;
1076
+ }
1077
+ else {
1078
+ if (descriptor !== undefined)
1079
+ throw new TypeError("Content source conflicts with an existing reference");
1080
+ parent[last] = source;
1081
+ }
1082
+ }
1083
+ }
1084
+ //# sourceMappingURL=hooks.js.map