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,28 @@
|
|
|
1
|
+
/** Harness-facing, configuration-driven API for the complete canonical draft catalogue. */
|
|
2
|
+
export { Hooks } from "./hooks.js";
|
|
3
|
+
export * from "./types.js";
|
|
4
|
+
export * from "./auth.js";
|
|
5
|
+
export * from "./content.js";
|
|
6
|
+
export * from "./composition.js";
|
|
7
|
+
export * from "./transport.js";
|
|
8
|
+
export type {
|
|
9
|
+
Registration,
|
|
10
|
+
Capabilities,
|
|
11
|
+
StaticCapabilityManifest,
|
|
12
|
+
Effect,
|
|
13
|
+
InterceptResponse,
|
|
14
|
+
Authentication,
|
|
15
|
+
ContentSelection,
|
|
16
|
+
ContentUpload,
|
|
17
|
+
} from "../draft/generated.js";
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
Permission,
|
|
21
|
+
state,
|
|
22
|
+
effects,
|
|
23
|
+
contentSlots,
|
|
24
|
+
capabilities,
|
|
25
|
+
events,
|
|
26
|
+
CapabilityBuilder,
|
|
27
|
+
} from "../draft/generated.js";
|
|
28
|
+
export type * from "../draft/generated.js";
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
|
+
import type {
|
|
3
|
+
Backend,
|
|
4
|
+
HttpTransport,
|
|
5
|
+
StdioTransport,
|
|
6
|
+
} from "../draft/generated.js";
|
|
7
|
+
import { HookOperationalError } from "../errors.js";
|
|
8
|
+
import { NdjsonDecoder } from "../framing.js";
|
|
9
|
+
|
|
10
|
+
type Fetcher = (url: string, init: RequestInit) => Promise<Response>;
|
|
11
|
+
type Pending = {
|
|
12
|
+
message: any;
|
|
13
|
+
notification: boolean;
|
|
14
|
+
resolve(value: unknown): void;
|
|
15
|
+
reject(error: unknown): void;
|
|
16
|
+
response?: unknown;
|
|
17
|
+
};
|
|
18
|
+
type ProcessState = {
|
|
19
|
+
child: ChildProcessWithoutNullStreams;
|
|
20
|
+
closed: Promise<void>;
|
|
21
|
+
pending: Map<unknown, Pending>;
|
|
22
|
+
stopped: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function failure(message: string): Error {
|
|
26
|
+
return new HookOperationalError("IO_ERROR", message);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Validate only the JSON-RPC envelope; the caller validates method-specific results. */
|
|
30
|
+
function responseFor(value: any, message: any): unknown {
|
|
31
|
+
if (
|
|
32
|
+
!value ||
|
|
33
|
+
typeof value !== "object" ||
|
|
34
|
+
Array.isArray(value) ||
|
|
35
|
+
value.jsonrpc !== "2.0" ||
|
|
36
|
+
"result" in value === "error" in value ||
|
|
37
|
+
"method" in value ||
|
|
38
|
+
!("id" in value)
|
|
39
|
+
) {
|
|
40
|
+
throw new HookOperationalError(
|
|
41
|
+
"MALFORMED_JSON_RPC",
|
|
42
|
+
"Expected one JSON-RPC response",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (value.id !== message.id) {
|
|
46
|
+
throw new HookOperationalError(
|
|
47
|
+
"ID_MISMATCH",
|
|
48
|
+
"Backend response ID does not match request",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (
|
|
52
|
+
"error" in value &&
|
|
53
|
+
(!value.error ||
|
|
54
|
+
typeof value.error !== "object" ||
|
|
55
|
+
!Number.isInteger(value.error.code) ||
|
|
56
|
+
typeof value.error.message !== "string")
|
|
57
|
+
) {
|
|
58
|
+
throw new HookOperationalError(
|
|
59
|
+
"MALFORMED_JSON_RPC",
|
|
60
|
+
"Malformed JSON-RPC error",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Client-side transport. Deadlines are supplied by the caller's AbortSignal. */
|
|
67
|
+
export class BackendTransport {
|
|
68
|
+
readonly #transport: StdioTransport | HttpTransport;
|
|
69
|
+
readonly #fetcher: Fetcher;
|
|
70
|
+
readonly #controllers = new Set<AbortController>();
|
|
71
|
+
readonly #processes = new Set<ProcessState>();
|
|
72
|
+
#process: ProcessState | undefined;
|
|
73
|
+
#tail: Promise<unknown> = Promise.resolve();
|
|
74
|
+
#closed = false;
|
|
75
|
+
#closing: Promise<void> | undefined;
|
|
76
|
+
|
|
77
|
+
constructor(backend: Backend, fetcher: Fetcher) {
|
|
78
|
+
const transport = backend.transport;
|
|
79
|
+
if (transport.type !== "http" && transport.type !== "stdio") {
|
|
80
|
+
throw new HookOperationalError(
|
|
81
|
+
"INVALID_CONFIG",
|
|
82
|
+
"Unsupported backend transport",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (
|
|
86
|
+
transport.type === "stdio" &&
|
|
87
|
+
transport.lifecycle !== "persistent" &&
|
|
88
|
+
transport.lifecycle !== "per_event"
|
|
89
|
+
) {
|
|
90
|
+
throw new HookOperationalError(
|
|
91
|
+
"INVALID_CONFIG",
|
|
92
|
+
"Unsupported stdio lifecycle",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
this.#transport = transport as StdioTransport | HttpTransport;
|
|
96
|
+
this.#fetcher = fetcher;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
request(message: any, signal?: AbortSignal): Promise<unknown> {
|
|
100
|
+
return this.#send(message, false, signal);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async notify(message: any, signal?: AbortSignal): Promise<void> {
|
|
104
|
+
await this.#send(message, true, signal);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#send(
|
|
108
|
+
message: any,
|
|
109
|
+
notification: boolean,
|
|
110
|
+
signal?: AbortSignal,
|
|
111
|
+
): Promise<unknown> {
|
|
112
|
+
if (this.#closed)
|
|
113
|
+
return Promise.reject(failure("Backend transport is closed"));
|
|
114
|
+
const controller = new AbortController();
|
|
115
|
+
const abort = () => controller.abort(signal?.reason);
|
|
116
|
+
if (signal?.aborted) abort();
|
|
117
|
+
else signal?.addEventListener("abort", abort, { once: true });
|
|
118
|
+
this.#controllers.add(controller);
|
|
119
|
+
let started = false;
|
|
120
|
+
const run = async () => {
|
|
121
|
+
controller.signal.throwIfAborted();
|
|
122
|
+
started = true;
|
|
123
|
+
return this.#transport.type === "http"
|
|
124
|
+
? this.#http(message, notification, controller.signal)
|
|
125
|
+
: this.#stdio(message, notification, controller.signal);
|
|
126
|
+
};
|
|
127
|
+
// This revision permits only one outstanding intercept per persistent
|
|
128
|
+
// backend. Keep the queue occupied through process shutdown on cancellation.
|
|
129
|
+
const stdio = this.#transport.type === "stdio";
|
|
130
|
+
const work = stdio ? this.#tail.then(run) : run();
|
|
131
|
+
if (stdio) this.#tail = work.catch(() => {});
|
|
132
|
+
// Also interrupt queued calls, and fetch implementations which ignore cancellation.
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
const cancel = () => reject(controller.signal.reason);
|
|
135
|
+
controller.signal.addEventListener("abort", cancel, { once: true });
|
|
136
|
+
if (controller.signal.aborted) cancel();
|
|
137
|
+
work.then(resolve, reject).finally(() => {
|
|
138
|
+
controller.signal.removeEventListener("abort", cancel);
|
|
139
|
+
});
|
|
140
|
+
}).finally(async () => {
|
|
141
|
+
// Retire queued work immediately, but reap an active SDK child before
|
|
142
|
+
// completing its cancelled operation. The queue remains occupied too.
|
|
143
|
+
if (stdio && started && controller.signal.aborted)
|
|
144
|
+
await work.catch(() => {});
|
|
145
|
+
signal?.removeEventListener("abort", abort);
|
|
146
|
+
this.#controllers.delete(controller);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async #http(
|
|
151
|
+
message: any,
|
|
152
|
+
notification: boolean,
|
|
153
|
+
signal: AbortSignal,
|
|
154
|
+
): Promise<unknown> {
|
|
155
|
+
const transport = this.#transport as HttpTransport;
|
|
156
|
+
const response = await this.#fetcher(transport.url, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: {
|
|
159
|
+
"Content-Type": "application/json",
|
|
160
|
+
Accept: "application/json",
|
|
161
|
+
},
|
|
162
|
+
body: JSON.stringify(message),
|
|
163
|
+
redirect: "error",
|
|
164
|
+
signal,
|
|
165
|
+
});
|
|
166
|
+
try {
|
|
167
|
+
signal.throwIfAborted();
|
|
168
|
+
if (notification) {
|
|
169
|
+
if (response.status !== 202 && response.status !== 204) {
|
|
170
|
+
throw failure(
|
|
171
|
+
`Unexpected notification HTTP status ${response.status}`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
await this.#responseText(response, signal, 0);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (response.status !== 200)
|
|
178
|
+
throw failure(`Unexpected request HTTP status ${response.status}`);
|
|
179
|
+
if (
|
|
180
|
+
response.headers
|
|
181
|
+
.get("content-type")
|
|
182
|
+
?.split(";")[0]
|
|
183
|
+
?.trim()
|
|
184
|
+
.toLowerCase() !== "application/json"
|
|
185
|
+
) {
|
|
186
|
+
throw new HookOperationalError(
|
|
187
|
+
"MALFORMED_JSON_RPC",
|
|
188
|
+
"Backend response must use application/json",
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
// Match the SDK's 1 MiB NDJSON frame budget rather than buffering unbounded JSON.
|
|
192
|
+
const text = await this.#responseText(response, signal, 1024 * 1024);
|
|
193
|
+
signal.throwIfAborted();
|
|
194
|
+
let decoded: unknown;
|
|
195
|
+
try {
|
|
196
|
+
decoded = JSON.parse(text);
|
|
197
|
+
} catch {
|
|
198
|
+
throw new HookOperationalError(
|
|
199
|
+
"MALFORMED_JSON",
|
|
200
|
+
"Malformed backend response",
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
return responseFor(decoded, message);
|
|
204
|
+
} finally {
|
|
205
|
+
// Also release rejected responses and late results from fetchers that ignore abort.
|
|
206
|
+
// A custom stream's cancellation hook must not hold a deadline or close hostage.
|
|
207
|
+
if (response.body && !response.body.locked)
|
|
208
|
+
void response.body.cancel().catch(() => {});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async #responseText(
|
|
213
|
+
response: Response,
|
|
214
|
+
signal: AbortSignal,
|
|
215
|
+
limit: number,
|
|
216
|
+
): Promise<string> {
|
|
217
|
+
if (!response.body) return "";
|
|
218
|
+
const reader = response.body.getReader();
|
|
219
|
+
const cancel = () => {
|
|
220
|
+
void reader.cancel(signal.reason).catch(() => {});
|
|
221
|
+
};
|
|
222
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
223
|
+
let bytes = 0;
|
|
224
|
+
let text = "";
|
|
225
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
226
|
+
try {
|
|
227
|
+
signal.throwIfAborted();
|
|
228
|
+
while (true) {
|
|
229
|
+
const chunk = await reader.read();
|
|
230
|
+
signal.throwIfAborted();
|
|
231
|
+
if (chunk.done) return text + decoder.decode();
|
|
232
|
+
bytes += chunk.value.byteLength;
|
|
233
|
+
if (bytes > limit)
|
|
234
|
+
throw failure(
|
|
235
|
+
limit === 0
|
|
236
|
+
? "Notification response must be empty"
|
|
237
|
+
: "HTTP JSON-RPC frame exceeds 1 MiB",
|
|
238
|
+
);
|
|
239
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
240
|
+
}
|
|
241
|
+
} finally {
|
|
242
|
+
signal.removeEventListener("abort", cancel);
|
|
243
|
+
void reader.cancel().catch(() => {});
|
|
244
|
+
reader.releaseLock();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#start(): ProcessState {
|
|
249
|
+
const transport = this.#transport as StdioTransport;
|
|
250
|
+
const child = spawn(transport.command, transport.args ?? [], {
|
|
251
|
+
cwd: transport.cwd,
|
|
252
|
+
shell: false,
|
|
253
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
254
|
+
});
|
|
255
|
+
let onClose!: () => void;
|
|
256
|
+
const state: ProcessState = {
|
|
257
|
+
child,
|
|
258
|
+
stopped: false,
|
|
259
|
+
pending: new Map(),
|
|
260
|
+
closed: new Promise<void>((resolve) => {
|
|
261
|
+
onClose = resolve;
|
|
262
|
+
}),
|
|
263
|
+
};
|
|
264
|
+
const decoder = new NdjsonDecoder();
|
|
265
|
+
const fail = (error: unknown) => {
|
|
266
|
+
for (const pending of state.pending.values()) pending.reject(error);
|
|
267
|
+
state.pending.clear();
|
|
268
|
+
this.#stop(state);
|
|
269
|
+
};
|
|
270
|
+
child.stderr.resume();
|
|
271
|
+
child.on("error", fail);
|
|
272
|
+
child.stdin.on("error", fail);
|
|
273
|
+
child.stdout.on("error", fail);
|
|
274
|
+
child.stdout.on("data", (chunk: Uint8Array) => {
|
|
275
|
+
try {
|
|
276
|
+
const lines = decoder.push(chunk);
|
|
277
|
+
for (const line of lines) {
|
|
278
|
+
const value = JSON.parse(line);
|
|
279
|
+
if (transport.lifecycle === "persistent") {
|
|
280
|
+
// Validate protocol output, but ignore replies to abandoned/unknown IDs.
|
|
281
|
+
const response = responseFor(value, { id: value?.id });
|
|
282
|
+
const pending = state.pending.get(value.id);
|
|
283
|
+
if (pending) pending.resolve(response);
|
|
284
|
+
} else {
|
|
285
|
+
const pending = state.pending.values().next().value;
|
|
286
|
+
if (
|
|
287
|
+
!pending ||
|
|
288
|
+
pending.notification ||
|
|
289
|
+
pending.response !== undefined
|
|
290
|
+
)
|
|
291
|
+
throw failure("Unexpected backend response");
|
|
292
|
+
pending.response = responseFor(value, pending.message);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
} catch (error) {
|
|
296
|
+
fail(error);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
child.on("close", (code: number | null) => {
|
|
300
|
+
try {
|
|
301
|
+
decoder.end();
|
|
302
|
+
for (const pending of state.pending.values()) {
|
|
303
|
+
if (code !== 0)
|
|
304
|
+
throw failure(`Backend exited with status ${String(code)}`);
|
|
305
|
+
if (!pending.notification) {
|
|
306
|
+
if (pending.response === undefined)
|
|
307
|
+
throw failure("Backend exited without a response");
|
|
308
|
+
}
|
|
309
|
+
pending.resolve(pending.response);
|
|
310
|
+
}
|
|
311
|
+
} catch (error) {
|
|
312
|
+
for (const pending of state.pending.values()) pending.reject(error);
|
|
313
|
+
}
|
|
314
|
+
state.pending.clear();
|
|
315
|
+
state.stopped = true;
|
|
316
|
+
if (this.#process === state) this.#process = undefined;
|
|
317
|
+
this.#processes.delete(state);
|
|
318
|
+
onClose();
|
|
319
|
+
});
|
|
320
|
+
this.#processes.add(state);
|
|
321
|
+
this.#process = state;
|
|
322
|
+
return state;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#stop(state: ProcessState): void {
|
|
326
|
+
if (this.#process === state) this.#process = undefined;
|
|
327
|
+
if (!state.stopped) {
|
|
328
|
+
state.stopped = true;
|
|
329
|
+
// Do not let an uncooperative backend retain owned processes after cancellation.
|
|
330
|
+
state.child.kill("SIGKILL");
|
|
331
|
+
// Descendants can inherit these pipes even after the SDK child exits.
|
|
332
|
+
// Retire our handles so reaping does not wait for external descendants.
|
|
333
|
+
state.child.stdin.destroy();
|
|
334
|
+
state.child.stdout.destroy();
|
|
335
|
+
state.child.stderr.destroy();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async #stdio(
|
|
340
|
+
message: any,
|
|
341
|
+
notification: boolean,
|
|
342
|
+
signal: AbortSignal,
|
|
343
|
+
): Promise<unknown> {
|
|
344
|
+
const wire = JSON.stringify(message) + "\n";
|
|
345
|
+
const state = this.#process ?? this.#start();
|
|
346
|
+
const perEvent =
|
|
347
|
+
(this.#transport as StdioTransport).lifecycle === "per_event";
|
|
348
|
+
if (!notification && state.pending.has(message.id))
|
|
349
|
+
throw failure("Duplicate pending JSON-RPC request ID");
|
|
350
|
+
try {
|
|
351
|
+
return await new Promise<unknown>((resolve, reject) => {
|
|
352
|
+
let pending: Pending | undefined;
|
|
353
|
+
const cleanup = () => {
|
|
354
|
+
signal.removeEventListener("abort", abort);
|
|
355
|
+
if (pending && state.pending.get(message.id) === pending)
|
|
356
|
+
state.pending.delete(message.id);
|
|
357
|
+
};
|
|
358
|
+
const abort = () => {
|
|
359
|
+
cleanup();
|
|
360
|
+
reject(signal.reason);
|
|
361
|
+
// Abandoning an ID alone would leave the old intercept running when
|
|
362
|
+
// the queue advances. Kill and reap it before starting another call.
|
|
363
|
+
this.#stop(state);
|
|
364
|
+
};
|
|
365
|
+
const finish = (value: unknown) => {
|
|
366
|
+
cleanup();
|
|
367
|
+
resolve(value);
|
|
368
|
+
};
|
|
369
|
+
const fail = (error: unknown) => {
|
|
370
|
+
cleanup();
|
|
371
|
+
reject(error);
|
|
372
|
+
};
|
|
373
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
374
|
+
if (!notification || perEvent) {
|
|
375
|
+
pending = { message, notification, resolve: finish, reject: fail };
|
|
376
|
+
state.pending.set(message.id, pending);
|
|
377
|
+
}
|
|
378
|
+
const written = (error?: Error | null) => {
|
|
379
|
+
if (error) fail(error);
|
|
380
|
+
else if (notification && !perEvent) finish(undefined);
|
|
381
|
+
};
|
|
382
|
+
if (perEvent) state.child.stdin.end(wire, written);
|
|
383
|
+
else state.child.stdin.write(wire, written);
|
|
384
|
+
});
|
|
385
|
+
} catch (error) {
|
|
386
|
+
this.#stop(state);
|
|
387
|
+
throw error;
|
|
388
|
+
} finally {
|
|
389
|
+
if (perEvent || state.stopped) {
|
|
390
|
+
this.#stop(state);
|
|
391
|
+
await state.closed;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
close(): Promise<void> {
|
|
397
|
+
if (this.#closing) return this.#closing;
|
|
398
|
+
this.#closed = true;
|
|
399
|
+
for (const controller of this.#controllers)
|
|
400
|
+
controller.abort(failure("Backend transport is closed"));
|
|
401
|
+
const processes = [...this.#processes];
|
|
402
|
+
for (const state of processes) this.#stop(state);
|
|
403
|
+
this.#closing = Promise.all(processes.map((state) => state.closed)).then(
|
|
404
|
+
() => {},
|
|
405
|
+
);
|
|
406
|
+
return this.#closing;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
EventInputs,
|
|
3
|
+
EventType,
|
|
4
|
+
Permission,
|
|
5
|
+
ContentSourceBinding,
|
|
6
|
+
DeliveryDiagnosticCode,
|
|
7
|
+
} from "../draft/generated.js";
|
|
8
|
+
import type { ContentSource } from "./content.js";
|
|
9
|
+
import type {
|
|
10
|
+
Capabilities,
|
|
11
|
+
ContentReference,
|
|
12
|
+
ExecutionEventContextCompactBefore,
|
|
13
|
+
InterceptResponse,
|
|
14
|
+
InterceptRequest,
|
|
15
|
+
ObserveNotification,
|
|
16
|
+
StaticCapabilityManifest,
|
|
17
|
+
} from "../draft/generated.js";
|
|
18
|
+
import type { AuthProvider, DeliveryAuthProvider } from "./auth.js";
|
|
19
|
+
|
|
20
|
+
export type Event = ObserveNotification["params"]["event"];
|
|
21
|
+
export type { EventType } from "../draft/generated.js";
|
|
22
|
+
/** Remove generated JSON extension index signatures while preserving named fields. */
|
|
23
|
+
type Fields<T> = {
|
|
24
|
+
[K in keyof T as string extends K
|
|
25
|
+
? never
|
|
26
|
+
: number extends K
|
|
27
|
+
? never
|
|
28
|
+
: K]: T[K];
|
|
29
|
+
};
|
|
30
|
+
/** Harness content bodies may be owned streams instead of uploaded references. */
|
|
31
|
+
export type HarnessValue<T> = T extends ContentReference
|
|
32
|
+
? ContentReference | ContentSource | ReadableStream<Uint8Array>
|
|
33
|
+
: T extends readonly (infer V)[]
|
|
34
|
+
? HarnessValue<V>[]
|
|
35
|
+
: T extends { selection: string }
|
|
36
|
+
? Omit<
|
|
37
|
+
{ [K in keyof Fields<T>]: HarnessValue<Fields<T>[K]> },
|
|
38
|
+
"selection"
|
|
39
|
+
> & { selection?: T["selection"] }
|
|
40
|
+
: T extends object
|
|
41
|
+
? keyof Fields<T> extends never
|
|
42
|
+
? T
|
|
43
|
+
: { [K in keyof Fields<T>]: HarnessValue<Fields<T>[K]> }
|
|
44
|
+
: T;
|
|
45
|
+
// The generated compact-before JsonValue intersection includes impossible
|
|
46
|
+
// primitive/array event branches. Remove those branches before mapping content
|
|
47
|
+
// bodies; otherwise Omit sees their shared keys and loses canonical fields.
|
|
48
|
+
type CompactBeforeObject = Exclude<
|
|
49
|
+
ExecutionEventContextCompactBefore,
|
|
50
|
+
string | number | boolean | readonly unknown[]
|
|
51
|
+
>;
|
|
52
|
+
export type HarnessEvent<K extends EventType = EventType> =
|
|
53
|
+
K extends "context.compact.before"
|
|
54
|
+
? Omit<HarnessValue<CompactBeforeObject>, "trigger"> &
|
|
55
|
+
Pick<CompactBeforeObject, "trigger">
|
|
56
|
+
: HarnessValue<Extract<Event, { type: K }>>;
|
|
57
|
+
/** Passing a body stream transfers its read/cancel ownership to the SDK.
|
|
58
|
+
* Keep host execution data separately; canonical effects carry accepted rewrites. */
|
|
59
|
+
export type BoundaryInput<K extends EventType> = K extends EventType
|
|
60
|
+
? Omit<HarnessEvent<K>, "type" | "source" | "id" | "time" | "manifest"> & {
|
|
61
|
+
id?: string;
|
|
62
|
+
time?: string;
|
|
63
|
+
}
|
|
64
|
+
: never;
|
|
65
|
+
/** Generated, flattened host facts for named boundary calls. */
|
|
66
|
+
export type EventInput<K extends EventType> = HarnessValue<EventInputs[K]>;
|
|
67
|
+
/** Explicit delivery authority for one event. Effects remain separately granted. */
|
|
68
|
+
export interface EventGrant {
|
|
69
|
+
modes: ("intercept" | "observe")[];
|
|
70
|
+
capabilities?: Capabilities;
|
|
71
|
+
}
|
|
72
|
+
/** A plain capability value grants interception only. Omitted events grant nothing. */
|
|
73
|
+
export type EventCapabilities = Partial<
|
|
74
|
+
Record<EventType, Capabilities | EventGrant>
|
|
75
|
+
>;
|
|
76
|
+
export interface HooksOptions {
|
|
77
|
+
source: string;
|
|
78
|
+
/** A static manifest, or explicit event declarations. Plain Capabilities
|
|
79
|
+
* entries grant intercept only; use EventGrant to opt into observe. Omitted
|
|
80
|
+
* events, delivery modes, effects and elicitation form/url grants stay absent. */
|
|
81
|
+
capabilities: StaticCapabilityManifest | EventCapabilities;
|
|
82
|
+
auth?: AuthProvider | DeliveryAuthProvider;
|
|
83
|
+
/** Trusted HTTP network adapter (for example, host-managed TLS). The SDK still
|
|
84
|
+
* owns protocol/authentication and cancellation. Scope transport credentials
|
|
85
|
+
* to their exact destination; upload requests must not inherit event TLS identity.
|
|
86
|
+
* OAuth discovery/token requests use the separate auth({ fetch }) override. */
|
|
87
|
+
fetch?: typeof globalThis.fetch;
|
|
88
|
+
/** Bound the immutable raw-content snapshot held by each boundary. */
|
|
89
|
+
maxContentBytes?: number;
|
|
90
|
+
/** Best-effort observation budget, bounded by the operation signal. */
|
|
91
|
+
observationTimeoutMs?: number;
|
|
92
|
+
}
|
|
93
|
+
export interface BoundaryOptions {
|
|
94
|
+
/** Generated named slots bind owned sources to producer descriptors, without refs. */
|
|
95
|
+
contentSources?: readonly ContentSourceBinding<
|
|
96
|
+
ContentSource | ReadableStream<Uint8Array>
|
|
97
|
+
>[];
|
|
98
|
+
/** Canonical pending state before any interceptor runs. */
|
|
99
|
+
initialState?: InterceptRequest["params"]["state"];
|
|
100
|
+
/** One operation budget: queue, authentication, content, delivery and observations. */
|
|
101
|
+
signal?: AbortSignal;
|
|
102
|
+
/** Complete narrowed advertisement; omitted controls stay absent. */
|
|
103
|
+
capabilities?: Capabilities;
|
|
104
|
+
}
|
|
105
|
+
/** Structured SDK delivery evidence; messages and credential material are excluded. */
|
|
106
|
+
export interface DeliveryDiagnostic extends Omit<DeliveryError, "code"> {
|
|
107
|
+
code: DeliveryDiagnosticCode;
|
|
108
|
+
}
|
|
109
|
+
export interface DeliveryError {
|
|
110
|
+
backendId: string;
|
|
111
|
+
subscriptionIndex: number;
|
|
112
|
+
phase: "preparation" | "interception" | "observation";
|
|
113
|
+
code:
|
|
114
|
+
| "PREPARATION_FAILED"
|
|
115
|
+
| "DELIVERY_FAILED"
|
|
116
|
+
| "DEADLINE_EXCEEDED"
|
|
117
|
+
| "INTERRUPTED";
|
|
118
|
+
failurePolicy?: "fail-open" | "fail-closed";
|
|
119
|
+
syntheticDenial: boolean;
|
|
120
|
+
}
|
|
121
|
+
/** Immutable JSON snapshot, including nested candidate values and extensions. */
|
|
122
|
+
// Bound recursive JSON extension types for TypeScript; runtime freezing has no
|
|
123
|
+
// depth limit. Six levels cover the canonical containers and nested JSON data.
|
|
124
|
+
type StateDepth = [0, 0, 1, 2, 3, 4, 5];
|
|
125
|
+
type ReadonlyState<T, D extends number = 6> = T extends
|
|
126
|
+
| string
|
|
127
|
+
| number
|
|
128
|
+
| boolean
|
|
129
|
+
| null
|
|
130
|
+
| undefined
|
|
131
|
+
? T
|
|
132
|
+
: D extends 0
|
|
133
|
+
? Readonly<T>
|
|
134
|
+
: T extends object
|
|
135
|
+
? { readonly [P in keyof T]: ReadonlyState<T[P], StateDepth[D]> }
|
|
136
|
+
: T;
|
|
137
|
+
/** Canonical pending state after the last accepted settlement. */
|
|
138
|
+
export type BoundaryState = ReadonlyState<
|
|
139
|
+
NonNullable<InterceptRequest["params"]["state"]>
|
|
140
|
+
>;
|
|
141
|
+
export interface BoundaryResult<K extends EventType = EventType> {
|
|
142
|
+
/** Accepted effective input, with occurrence identity and static envelope supplied.
|
|
143
|
+
* Body streams are owned delivery resources, not reusable output streams. */
|
|
144
|
+
event: HarnessEvent<K>;
|
|
145
|
+
/** Effective canonical effects. The harness enacts them; the SDK executes no operation. */
|
|
146
|
+
response: InterceptResponse;
|
|
147
|
+
/** Detached, deeply frozen canonical state; no effects replay is needed.
|
|
148
|
+
* An interrupted boundary must not execute, even if earlier accepted state
|
|
149
|
+
* contains a candidate or permission. With no acceptance, initialState is
|
|
150
|
+
* preserved (or the neutral candidate/permission state is returned). */
|
|
151
|
+
readonly state: BoundaryState;
|
|
152
|
+
/** Canonical settled permission. None is not approval; interruption forbids execution. */
|
|
153
|
+
readonly permission: `${Permission}`;
|
|
154
|
+
/** Accepted tool arguments, not the original proposal. Validate in host code before use. */
|
|
155
|
+
readonly input: unknown;
|
|
156
|
+
/** All interception and observation delivery diagnostics. */
|
|
157
|
+
diagnostics: DeliveryDiagnostic[];
|
|
158
|
+
errors: DeliveryError[];
|
|
159
|
+
/** Compatibility view of observation diagnostics; already settled when the call returns. */
|
|
160
|
+
observations: Promise<DeliveryError[]>;
|
|
161
|
+
interrupted: boolean;
|
|
162
|
+
}
|
|
163
|
+
export interface ConfigurationIssue {
|
|
164
|
+
path: string;
|
|
165
|
+
code: string;
|
|
166
|
+
}
|
|
167
|
+
export class ConfigurationError extends Error {
|
|
168
|
+
readonly code = "INVALID_CONFIGURATION";
|
|
169
|
+
constructor(readonly issues: readonly ConfigurationIssue[]) {
|
|
170
|
+
super("Hook configuration could not be resolved");
|
|
171
|
+
this.name = "ConfigurationError";
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
2
|
+
import { fullFormats } from "ajv-formats/dist/formats.js";
|
|
3
|
+
import { schemas } from "../draft/schemas.js";
|
|
4
|
+
|
|
5
|
+
let registry: Ajv2020 | undefined;
|
|
6
|
+
/** Validate against the checked-in canonical schema without coercion. */
|
|
7
|
+
export function validateWire(name: string, value: unknown): string[] {
|
|
8
|
+
if (!registry) {
|
|
9
|
+
registry = new Ajv2020({
|
|
10
|
+
allErrors: true,
|
|
11
|
+
strict: false,
|
|
12
|
+
ownProperties: true,
|
|
13
|
+
});
|
|
14
|
+
registry.addFormat("uri", fullFormats.uri);
|
|
15
|
+
registry.addFormat("date-time", fullFormats["date-time"]);
|
|
16
|
+
for (const schema of schemas) registry.addSchema(schema);
|
|
17
|
+
}
|
|
18
|
+
const validate = registry.getSchema(
|
|
19
|
+
`https://agenthooksprotocol.org/schemas/draft/${name}.schema.json`,
|
|
20
|
+
);
|
|
21
|
+
if (!validate) return ["Unknown protocol schema"];
|
|
22
|
+
if (validate(value)) return [];
|
|
23
|
+
return (validate.errors ?? []).map((e) => `${e.instancePath}: ${e.keyword}`);
|
|
24
|
+
}
|