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