@vallum/standards 0.0.0-prerelease → 0.0.1-prerelease.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/a2aHttp.d.ts +25 -3
- package/dist/a2aHttp.js +162 -33
- package/dist/a2aNodeServer.js +127 -5
- package/dist/a2aPush.d.ts +3 -0
- package/dist/a2aPush.js +21 -0
- package/dist/a2aTask.d.ts +11 -3
- package/dist/a2aTask.js +37 -1
- package/package.json +5 -5
package/dist/a2aHttp.d.ts
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import { type A2AAgentCardWellKnownOptions } from "@vallum/registry";
|
|
2
2
|
import type { AgentActionPolicy } from "@vallum/policy-gateway";
|
|
3
3
|
import { type A2APushNotificationTransport, type A2ATaskPushNotificationConfig, type LocalA2APushNotificationStore } from "./a2aPush.js";
|
|
4
|
-
import { type A2ATask, type A2AProcessMessageContext, type A2AProcessMessageResult, type LocalA2ATaskStore } from "./a2aTask.js";
|
|
4
|
+
import { type A2ATask, type A2AMessage, type A2AProcessMessageContext, type A2AProcessMessageResult, type LocalA2ATaskStore } from "./a2aTask.js";
|
|
5
|
+
export type A2AHttpStandardsBodyMode = "vallum" | "a2a";
|
|
6
|
+
export type A2AHttpA2ATaskBody = Omit<A2ATask, "agenticVallum">;
|
|
5
7
|
export interface A2AHttpRequest {
|
|
6
8
|
readonly method?: string;
|
|
7
9
|
readonly path?: string;
|
|
8
10
|
readonly headers?: Record<string, string | undefined>;
|
|
9
11
|
readonly body?: unknown;
|
|
10
12
|
}
|
|
11
|
-
export type A2AHttpResponseBody = {
|
|
13
|
+
export type A2AHttpResponseBody = A2AHttpA2ATaskBody | {
|
|
14
|
+
readonly message: A2AMessage;
|
|
15
|
+
} | {
|
|
16
|
+
readonly task: A2AHttpA2ATaskBody;
|
|
17
|
+
} | {
|
|
18
|
+
readonly tasks: readonly A2AHttpA2ATaskBody[];
|
|
19
|
+
readonly nextPageToken?: string;
|
|
20
|
+
readonly pageSize?: number;
|
|
21
|
+
readonly totalSize?: number;
|
|
22
|
+
} | {
|
|
12
23
|
readonly kind: "agent-card";
|
|
13
24
|
readonly [key: string]: unknown;
|
|
14
25
|
} | {
|
|
@@ -32,8 +43,16 @@ export type A2AHttpResponseBody = {
|
|
|
32
43
|
readonly deleted: boolean;
|
|
33
44
|
} | {
|
|
34
45
|
readonly error: {
|
|
35
|
-
readonly code:
|
|
46
|
+
readonly code: number;
|
|
47
|
+
readonly status: string;
|
|
36
48
|
readonly message: string;
|
|
49
|
+
readonly details: readonly {
|
|
50
|
+
readonly "@type": "type.googleapis.com/google.rpc.ErrorInfo";
|
|
51
|
+
readonly reason: string;
|
|
52
|
+
readonly domain: "a2a-protocol.org";
|
|
53
|
+
readonly metadata?: Record<string, string>;
|
|
54
|
+
}[];
|
|
55
|
+
readonly reason: A2AHttpErrorCode | string;
|
|
37
56
|
};
|
|
38
57
|
};
|
|
39
58
|
export interface A2AHttpResponse {
|
|
@@ -54,10 +73,13 @@ export interface LocalA2AHttpHandlerOptions {
|
|
|
54
73
|
readonly pushNotificationStore?: LocalA2APushNotificationStore;
|
|
55
74
|
readonly pushNotificationTransport?: A2APushNotificationTransport;
|
|
56
75
|
readonly now?: () => Date;
|
|
76
|
+
readonly standardsBodyMode?: A2AHttpStandardsBodyMode;
|
|
77
|
+
readonly allowUnmanifestedTasks?: boolean;
|
|
57
78
|
readonly processMessage?: (context: A2AProcessMessageContext) => Promise<A2AProcessMessageResult> | A2AProcessMessageResult;
|
|
58
79
|
}
|
|
59
80
|
export declare const A2A_HTTP_SEND_MESSAGE_PATH: "/message:send";
|
|
60
81
|
export declare const A2A_HTTP_STREAM_MESSAGE_PATH: "/message:stream";
|
|
61
82
|
export declare const A2A_HTTP_EXTENDED_AGENT_CARD_PATH: "/extendedAgentCard";
|
|
62
83
|
export declare const A2A_HTTP_TASKS_PATH: "/tasks";
|
|
84
|
+
export declare const A2A_HTTP_SUBSCRIBE_TASK_SUFFIX: ":subscribe";
|
|
63
85
|
export declare function handleLocalA2AHttpRequest(request: A2AHttpRequest, options: LocalA2AHttpHandlerOptions): Promise<A2AHttpResponse>;
|
package/dist/a2aHttp.js
CHANGED
|
@@ -5,6 +5,7 @@ export const A2A_HTTP_SEND_MESSAGE_PATH = "/message:send";
|
|
|
5
5
|
export const A2A_HTTP_STREAM_MESSAGE_PATH = "/message:stream";
|
|
6
6
|
export const A2A_HTTP_EXTENDED_AGENT_CARD_PATH = "/extendedAgentCard";
|
|
7
7
|
export const A2A_HTTP_TASKS_PATH = "/tasks";
|
|
8
|
+
export const A2A_HTTP_SUBSCRIBE_TASK_SUFFIX = ":subscribe";
|
|
8
9
|
export async function handleLocalA2AHttpRequest(request, options) {
|
|
9
10
|
const method = normalizeMethod(request.method);
|
|
10
11
|
const url = parsePath(request.path);
|
|
@@ -39,32 +40,41 @@ export async function handleLocalA2AHttpRequest(request, options) {
|
|
|
39
40
|
if (url.pathname === A2A_HTTP_TASKS_PATH) {
|
|
40
41
|
if (method !== "GET")
|
|
41
42
|
return methodNotAllowed(["GET"]);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}),
|
|
52
|
-
});
|
|
43
|
+
const pageSize = numberQuery(url, "pageSize");
|
|
44
|
+
return taskListResponse(listA2ATasks({
|
|
45
|
+
store: options.store,
|
|
46
|
+
contextId: optionalQuery(url, "contextId"),
|
|
47
|
+
state: optionalQuery(url, "state"),
|
|
48
|
+
includeArtifacts: booleanQuery(url, "includeArtifacts"),
|
|
49
|
+
historyLength: numberQuery(url, "historyLength"),
|
|
50
|
+
pageSize,
|
|
51
|
+
}), options, pageSize);
|
|
53
52
|
}
|
|
54
53
|
const taskRoute = matchTaskRoute(url.pathname);
|
|
55
54
|
if (taskRoute) {
|
|
56
55
|
if (taskRoute.action === "get") {
|
|
57
56
|
if (method !== "GET")
|
|
58
57
|
return methodNotAllowed(["GET"]);
|
|
59
|
-
return
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
58
|
+
return taskResponse(getA2ATask({
|
|
59
|
+
store: options.store,
|
|
60
|
+
id: taskRoute.taskId,
|
|
61
|
+
includeArtifacts: booleanQuery(url, "includeArtifacts"),
|
|
62
|
+
historyLength: numberQuery(url, "historyLength"),
|
|
63
|
+
}), options);
|
|
64
|
+
}
|
|
65
|
+
if (taskRoute.action === "subscribe") {
|
|
66
|
+
if (method !== "GET" && method !== "POST")
|
|
67
|
+
return methodNotAllowed(["GET", "POST"]);
|
|
68
|
+
const result = getA2ATask({
|
|
69
|
+
store: options.store,
|
|
70
|
+
id: taskRoute.taskId,
|
|
71
|
+
includeArtifacts: booleanQuery(url, "includeArtifacts"),
|
|
72
|
+
historyLength: numberQuery(url, "historyLength"),
|
|
67
73
|
});
|
|
74
|
+
if (options.standardsBodyMode === "a2a" && isTerminalTaskState(result.task.status.state)) {
|
|
75
|
+
throw new A2ATaskError("A2A_TASK_TERMINAL", "Terminal A2A tasks cannot be subscribed.", 409);
|
|
76
|
+
}
|
|
77
|
+
return taskResponse(result, options);
|
|
68
78
|
}
|
|
69
79
|
if (method !== "POST")
|
|
70
80
|
return methodNotAllowed(["POST"]);
|
|
@@ -75,10 +85,7 @@ export async function handleLocalA2AHttpRequest(request, options) {
|
|
|
75
85
|
includeArtifacts: booleanQuery(url, "includeArtifacts"),
|
|
76
86
|
});
|
|
77
87
|
await maybeDeliverPushNotifications(result.task, options);
|
|
78
|
-
return
|
|
79
|
-
kind: "task",
|
|
80
|
-
...result,
|
|
81
|
-
});
|
|
88
|
+
return taskResponse(result, options);
|
|
82
89
|
}
|
|
83
90
|
return errorResponse(404, "A2A_ROUTE_NOT_FOUND", "A2A route was not found.");
|
|
84
91
|
}
|
|
@@ -137,13 +144,19 @@ function handleExtendedAgentCardRequest(options) {
|
|
|
137
144
|
});
|
|
138
145
|
}
|
|
139
146
|
async function handleSendMessage(request, options) {
|
|
140
|
-
|
|
141
|
-
|
|
147
|
+
const contentType = header(request.headers, "content-type");
|
|
148
|
+
if (contentType !== undefined && !isSupportedRequestContentType(contentType)) {
|
|
149
|
+
return errorResponse(415, "A2A_CONTENT_TYPE_NOT_SUPPORTED", "A2A request content type is not supported.");
|
|
142
150
|
}
|
|
143
151
|
const body = parseBody(request.body);
|
|
144
152
|
if (!isSendMessageBody(body)) {
|
|
145
153
|
return errorResponse(400, "A2A_BODY_INVALID", "A2A request body is invalid.");
|
|
146
154
|
}
|
|
155
|
+
const isNewTask = body.message.taskId === undefined;
|
|
156
|
+
const canCreateUnmanifestedTask = options.allowUnmanifestedTasks && body.manifest === undefined;
|
|
157
|
+
if (isNewTask && !options.taskPolicy && !canCreateUnmanifestedTask) {
|
|
158
|
+
return errorResponse(503, "A2A_POLICY_NOT_CONFIGURED", "A2A task endpoint policy is not configured.");
|
|
159
|
+
}
|
|
147
160
|
const result = await sendA2AMessage({
|
|
148
161
|
store: options.store,
|
|
149
162
|
protocolVersion: body.protocolVersion ?? A2A_TASK_PROTOCOL_VERSION,
|
|
@@ -152,17 +165,16 @@ async function handleSendMessage(request, options) {
|
|
|
152
165
|
policy: body.message.taskId ? undefined : options.taskPolicy,
|
|
153
166
|
now: options.now?.(),
|
|
154
167
|
contextId: body.contextId,
|
|
168
|
+
allowUnmanifestedTasks: options.allowUnmanifestedTasks,
|
|
169
|
+
returnImmediately: body.configuration?.returnImmediately,
|
|
155
170
|
processMessage: options.processMessage,
|
|
156
171
|
});
|
|
157
172
|
await maybeDeliverPushNotifications(result.task, options);
|
|
158
|
-
return
|
|
159
|
-
kind: "task",
|
|
160
|
-
...result,
|
|
161
|
-
});
|
|
173
|
+
return sendMessageResponse(result, options);
|
|
162
174
|
}
|
|
163
175
|
function handlePushNotificationRoute(method, url, request, options, route) {
|
|
164
176
|
if (!options.pushNotificationStore) {
|
|
165
|
-
return errorResponse(501, "A2A_OPERATION_UNSUPPORTED", "A2A push notification configuration is not enabled for this local Vallum server.");
|
|
177
|
+
return errorResponse(options.standardsBodyMode === "a2a" ? 400 : 501, options.standardsBodyMode === "a2a" ? "A2A_PUSH_NOTIFICATION_NOT_SUPPORTED" : "A2A_OPERATION_UNSUPPORTED", "A2A push notification configuration is not enabled for this local Vallum server.");
|
|
166
178
|
}
|
|
167
179
|
getA2ATask({ store: options.store, id: route.taskId });
|
|
168
180
|
if (!route.configId) {
|
|
@@ -211,6 +223,14 @@ function handlePushNotificationRoute(method, url, request, options, route) {
|
|
|
211
223
|
}
|
|
212
224
|
return methodNotAllowed(["DELETE", "GET"]);
|
|
213
225
|
}
|
|
226
|
+
function isTerminalTaskState(state) {
|
|
227
|
+
return [
|
|
228
|
+
"TASK_STATE_COMPLETED",
|
|
229
|
+
"TASK_STATE_CANCELED",
|
|
230
|
+
"TASK_STATE_FAILED",
|
|
231
|
+
"TASK_STATE_REJECTED",
|
|
232
|
+
].includes(state);
|
|
233
|
+
}
|
|
214
234
|
async function maybeDeliverPushNotifications(task, options) {
|
|
215
235
|
if (!options.pushNotificationStore || !options.pushNotificationTransport)
|
|
216
236
|
return;
|
|
@@ -237,19 +257,73 @@ function validateProtocolVersion(request) {
|
|
|
237
257
|
}
|
|
238
258
|
return undefined;
|
|
239
259
|
}
|
|
240
|
-
function ok(body) {
|
|
260
|
+
function ok(body, contentType = A2A_TASK_MEDIA_TYPE) {
|
|
241
261
|
return {
|
|
242
262
|
status: 200,
|
|
243
263
|
headers: {
|
|
244
|
-
"content-type": `${
|
|
264
|
+
"content-type": `${contentType}; charset=utf-8`,
|
|
245
265
|
"cache-control": "no-store",
|
|
246
266
|
},
|
|
247
267
|
body,
|
|
248
268
|
json: `${JSON.stringify(body)}\n`,
|
|
249
269
|
};
|
|
250
270
|
}
|
|
271
|
+
function sendMessageResponse(result, options) {
|
|
272
|
+
if (options.standardsBodyMode === "a2a") {
|
|
273
|
+
if (result.message) {
|
|
274
|
+
return ok({ message: result.message }, "application/json");
|
|
275
|
+
}
|
|
276
|
+
return ok({ task: a2aTaskBody(result.task) }, "application/json");
|
|
277
|
+
}
|
|
278
|
+
return ok({
|
|
279
|
+
kind: "task",
|
|
280
|
+
...result,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
function taskResponse(result, options) {
|
|
284
|
+
if (options.standardsBodyMode === "a2a") {
|
|
285
|
+
return ok(a2aTaskBody(result.task), "application/json");
|
|
286
|
+
}
|
|
287
|
+
return ok({
|
|
288
|
+
kind: "task",
|
|
289
|
+
...result,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
function taskListResponse(result, options, pageSize) {
|
|
293
|
+
if (options.standardsBodyMode === "a2a") {
|
|
294
|
+
return ok({
|
|
295
|
+
tasks: result.tasks.map(a2aTaskBody),
|
|
296
|
+
nextPageToken: "",
|
|
297
|
+
pageSize: pageSize ?? result.tasks.length,
|
|
298
|
+
totalSize: result.tasks.length,
|
|
299
|
+
}, "application/json");
|
|
300
|
+
}
|
|
301
|
+
return ok({
|
|
302
|
+
kind: "task-list",
|
|
303
|
+
...result,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
function a2aTaskBody(task) {
|
|
307
|
+
const { agenticVallum: _agenticVallum, ...rest } = task;
|
|
308
|
+
return rest;
|
|
309
|
+
}
|
|
251
310
|
function errorResponse(status, code, message, headers = {}) {
|
|
252
|
-
const body = {
|
|
311
|
+
const body = {
|
|
312
|
+
error: {
|
|
313
|
+
code: status,
|
|
314
|
+
status: httpStatusName(status),
|
|
315
|
+
message,
|
|
316
|
+
details: [{
|
|
317
|
+
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
|
|
318
|
+
reason: a2aErrorInfoReason(code),
|
|
319
|
+
domain: "a2a-protocol.org",
|
|
320
|
+
metadata: {
|
|
321
|
+
vallumCode: code,
|
|
322
|
+
},
|
|
323
|
+
}],
|
|
324
|
+
reason: code,
|
|
325
|
+
},
|
|
326
|
+
};
|
|
253
327
|
return {
|
|
254
328
|
status,
|
|
255
329
|
headers: {
|
|
@@ -264,6 +338,51 @@ function errorResponse(status, code, message, headers = {}) {
|
|
|
264
338
|
function methodNotAllowed(allowed) {
|
|
265
339
|
return errorResponse(405, "A2A_METHOD_NOT_ALLOWED", "A2A route does not support this method.", { allow: allowed.join(", ") });
|
|
266
340
|
}
|
|
341
|
+
function httpStatusName(status) {
|
|
342
|
+
switch (status) {
|
|
343
|
+
case 400:
|
|
344
|
+
return "INVALID_ARGUMENT";
|
|
345
|
+
case 401:
|
|
346
|
+
return "UNAUTHENTICATED";
|
|
347
|
+
case 404:
|
|
348
|
+
return "NOT_FOUND";
|
|
349
|
+
case 405:
|
|
350
|
+
return "METHOD_NOT_ALLOWED";
|
|
351
|
+
case 409:
|
|
352
|
+
return "FAILED_PRECONDITION";
|
|
353
|
+
case 410:
|
|
354
|
+
return "ABORTED";
|
|
355
|
+
case 415:
|
|
356
|
+
return "INVALID_ARGUMENT";
|
|
357
|
+
case 501:
|
|
358
|
+
return "UNIMPLEMENTED";
|
|
359
|
+
case 503:
|
|
360
|
+
return "UNAVAILABLE";
|
|
361
|
+
case 200:
|
|
362
|
+
return "OK";
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function a2aErrorInfoReason(code) {
|
|
366
|
+
switch (code) {
|
|
367
|
+
case "A2A_TASK_NOT_FOUND":
|
|
368
|
+
return "TASK_NOT_FOUND";
|
|
369
|
+
case "A2A_TASK_NOT_CANCELABLE":
|
|
370
|
+
return "TASK_NOT_CANCELABLE";
|
|
371
|
+
case "A2A_PUSH_NOT_ENABLED":
|
|
372
|
+
case "A2A_OPERATION_UNSUPPORTED":
|
|
373
|
+
return "UNSUPPORTED_OPERATION";
|
|
374
|
+
case "A2A_PUSH_NOTIFICATION_NOT_SUPPORTED":
|
|
375
|
+
return "PUSH_NOTIFICATION_NOT_SUPPORTED";
|
|
376
|
+
case "A2A_EXTENDED_AGENT_CARD_NOT_CONFIGURED":
|
|
377
|
+
return "EXTENDED_AGENT_CARD_NOT_CONFIGURED";
|
|
378
|
+
case "A2A_VERSION_NOT_SUPPORTED":
|
|
379
|
+
return "VERSION_NOT_SUPPORTED";
|
|
380
|
+
case "A2A_CONTENT_TYPE_NOT_SUPPORTED":
|
|
381
|
+
return "CONTENT_TYPE_NOT_SUPPORTED";
|
|
382
|
+
default:
|
|
383
|
+
return "INVALID_REQUEST";
|
|
384
|
+
}
|
|
385
|
+
}
|
|
267
386
|
function parseBody(body) {
|
|
268
387
|
if (typeof body !== "string")
|
|
269
388
|
return body;
|
|
@@ -279,6 +398,10 @@ function isSendMessageBody(value) {
|
|
|
279
398
|
return false;
|
|
280
399
|
return isRecord(value.message);
|
|
281
400
|
}
|
|
401
|
+
function isSupportedRequestContentType(contentType) {
|
|
402
|
+
const mediaType = contentType.split(";")[0]?.trim().toLowerCase();
|
|
403
|
+
return mediaType === "application/json" || mediaType === A2A_TASK_MEDIA_TYPE;
|
|
404
|
+
}
|
|
282
405
|
function matchTaskRoute(pathname) {
|
|
283
406
|
const parts = pathname.split("/").filter(Boolean);
|
|
284
407
|
if (parts.length !== 2 || parts[0] !== "tasks")
|
|
@@ -287,6 +410,12 @@ function matchTaskRoute(pathname) {
|
|
|
287
410
|
if (taskPart.endsWith(":cancel")) {
|
|
288
411
|
return { taskId: decodeURIComponent(taskPart.slice(0, -":cancel".length)), action: "cancel" };
|
|
289
412
|
}
|
|
413
|
+
if (taskPart.endsWith(A2A_HTTP_SUBSCRIBE_TASK_SUFFIX)) {
|
|
414
|
+
return {
|
|
415
|
+
taskId: decodeURIComponent(taskPart.slice(0, -A2A_HTTP_SUBSCRIBE_TASK_SUFFIX.length)),
|
|
416
|
+
action: "subscribe",
|
|
417
|
+
};
|
|
418
|
+
}
|
|
290
419
|
return { taskId: decodeURIComponent(taskPart), action: "get" };
|
|
291
420
|
}
|
|
292
421
|
function matchPushNotificationRoute(pathname) {
|
package/dist/a2aNodeServer.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { A2A_JWKS_WELL_KNOWN_PATH, handleA2APublicJwksRequest, } from "@vallum/registry";
|
|
3
|
-
import { A2A_HTTP_SEND_MESSAGE_PATH, A2A_HTTP_STREAM_MESSAGE_PATH, handleLocalA2AHttpRequest, } from "./a2aHttp.js";
|
|
3
|
+
import { A2A_HTTP_SEND_MESSAGE_PATH, A2A_HTTP_SUBSCRIBE_TASK_SUFFIX, A2A_HTTP_STREAM_MESSAGE_PATH, handleLocalA2AHttpRequest, } from "./a2aHttp.js";
|
|
4
4
|
const DEFAULT_HOST = "127.0.0.1";
|
|
5
5
|
const DEFAULT_MAX_BODY_BYTES = 64_000;
|
|
6
6
|
export async function startLocalA2ANodeServer(options) {
|
|
@@ -8,6 +8,7 @@ export async function startLocalA2ANodeServer(options) {
|
|
|
8
8
|
if (!options.allowNonLoopbackHost && !isLoopbackHost(host)) {
|
|
9
9
|
throw new Error("A2A local Node server refuses non-loopback hosts without explicit opt-in.");
|
|
10
10
|
}
|
|
11
|
+
const subscribers = new Map();
|
|
11
12
|
const server = createServer(async (request, response) => {
|
|
12
13
|
try {
|
|
13
14
|
const body = await readBody(request, options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES);
|
|
@@ -30,7 +31,27 @@ export async function startLocalA2ANodeServer(options) {
|
|
|
30
31
|
writeHandledResponse(response, handled.status, handled.headers, handled.json);
|
|
31
32
|
return;
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
+
notifySubscribers(subscribers, handled.body, options);
|
|
35
|
+
writeServerSentEvents(response, [{ event: "task", data: streamResponseBody(handled.body, options) }]);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (isSubscribeTaskPath(requestPathname(request.url))) {
|
|
39
|
+
const handled = await handleLocalA2AHttpRequest({
|
|
40
|
+
method: request.method,
|
|
41
|
+
path: request.url,
|
|
42
|
+
headers: normalizeHeaders(request.headers),
|
|
43
|
+
body,
|
|
44
|
+
}, options);
|
|
45
|
+
if (handled.status !== 200) {
|
|
46
|
+
writeHandledResponse(response, handled.status, handled.headers, handled.json);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const task = a2aTaskFromResponseBody(handled.body);
|
|
50
|
+
if (!task) {
|
|
51
|
+
writeServerSentEvents(response, [{ event: "task", data: streamResponseBody(handled.body, options) }]);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
writeSubscribeResponse(response, task, subscribers, options);
|
|
34
55
|
return;
|
|
35
56
|
}
|
|
36
57
|
const handled = await handleLocalA2AHttpRequest({
|
|
@@ -39,6 +60,9 @@ export async function startLocalA2ANodeServer(options) {
|
|
|
39
60
|
headers: normalizeHeaders(request.headers),
|
|
40
61
|
body,
|
|
41
62
|
}, options);
|
|
63
|
+
if (handled.status === 200) {
|
|
64
|
+
notifySubscribers(subscribers, handled.body, options);
|
|
65
|
+
}
|
|
42
66
|
writeHandledResponse(response, handled.status, handled.headers, handled.json);
|
|
43
67
|
}
|
|
44
68
|
catch (error) {
|
|
@@ -68,7 +92,10 @@ export async function startLocalA2ANodeServer(options) {
|
|
|
68
92
|
host: address.address,
|
|
69
93
|
port: address.port,
|
|
70
94
|
boundToLoopback: isLoopbackHost(address.address),
|
|
71
|
-
close: () =>
|
|
95
|
+
close: async () => {
|
|
96
|
+
closeSubscribers(subscribers);
|
|
97
|
+
await closeServer(server);
|
|
98
|
+
},
|
|
72
99
|
};
|
|
73
100
|
}
|
|
74
101
|
function listen(server, host, port) {
|
|
@@ -99,11 +126,99 @@ function writeServerSentEvents(response, events) {
|
|
|
99
126
|
response.setHeader("connection", "keep-alive");
|
|
100
127
|
response.setHeader("x-accel-buffering", "no");
|
|
101
128
|
for (const event of events) {
|
|
102
|
-
response
|
|
103
|
-
response.write(`data: ${JSON.stringify(event.data)}\n\n`);
|
|
129
|
+
writeServerSentEvent(response, event);
|
|
104
130
|
}
|
|
105
131
|
response.end();
|
|
106
132
|
}
|
|
133
|
+
function writeSubscribeResponse(response, task, subscribers, options) {
|
|
134
|
+
response.statusCode = 200;
|
|
135
|
+
response.setHeader("content-type", "text/event-stream; charset=utf-8");
|
|
136
|
+
response.setHeader("cache-control", "no-store");
|
|
137
|
+
response.setHeader("connection", "keep-alive");
|
|
138
|
+
response.setHeader("x-accel-buffering", "no");
|
|
139
|
+
writeServerSentEvent(response, { event: "task", data: streamResponseBody(task, options) });
|
|
140
|
+
if (isTerminalTaskState(task.status.state)) {
|
|
141
|
+
response.end();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const taskSubscribers = subscribers.get(task.id) ?? new Set();
|
|
145
|
+
taskSubscribers.add(response);
|
|
146
|
+
subscribers.set(task.id, taskSubscribers);
|
|
147
|
+
response.once("close", () => {
|
|
148
|
+
taskSubscribers.delete(response);
|
|
149
|
+
if (taskSubscribers.size === 0)
|
|
150
|
+
subscribers.delete(task.id);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function notifySubscribers(subscribers, body, options) {
|
|
154
|
+
const task = a2aTaskFromResponseBody(body);
|
|
155
|
+
if (!task)
|
|
156
|
+
return;
|
|
157
|
+
const taskSubscribers = subscribers.get(task.id);
|
|
158
|
+
if (!taskSubscribers || taskSubscribers.size === 0)
|
|
159
|
+
return;
|
|
160
|
+
for (const subscriber of [...taskSubscribers]) {
|
|
161
|
+
if (subscriber.destroyed || subscriber.writableEnded) {
|
|
162
|
+
taskSubscribers.delete(subscriber);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
writeServerSentEvent(subscriber, { event: "task", data: streamResponseBody(task, options) });
|
|
166
|
+
if (isTerminalTaskState(task.status.state)) {
|
|
167
|
+
taskSubscribers.delete(subscriber);
|
|
168
|
+
subscriber.end();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (taskSubscribers.size === 0)
|
|
172
|
+
subscribers.delete(task.id);
|
|
173
|
+
}
|
|
174
|
+
function closeSubscribers(subscribers) {
|
|
175
|
+
for (const taskSubscribers of subscribers.values()) {
|
|
176
|
+
for (const subscriber of taskSubscribers) {
|
|
177
|
+
if (!subscriber.destroyed && !subscriber.writableEnded)
|
|
178
|
+
subscriber.end();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
subscribers.clear();
|
|
182
|
+
}
|
|
183
|
+
function writeServerSentEvent(response, event) {
|
|
184
|
+
response.write(`event: ${event.event}\n`);
|
|
185
|
+
response.write(`data: ${JSON.stringify(event.data)}\n\n`);
|
|
186
|
+
}
|
|
187
|
+
function streamResponseBody(body, options) {
|
|
188
|
+
if (options.standardsBodyMode !== "a2a")
|
|
189
|
+
return body;
|
|
190
|
+
const task = a2aTaskFromResponseBody(body);
|
|
191
|
+
if (!task)
|
|
192
|
+
return body;
|
|
193
|
+
return {
|
|
194
|
+
status_update: {
|
|
195
|
+
taskId: task.id,
|
|
196
|
+
contextId: task.contextId,
|
|
197
|
+
status: task.status,
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function a2aTaskFromResponseBody(body) {
|
|
202
|
+
if (!isRecord(body))
|
|
203
|
+
return undefined;
|
|
204
|
+
const record = body;
|
|
205
|
+
if (typeof record.id === "string" && isRecord(record.status)) {
|
|
206
|
+
return record;
|
|
207
|
+
}
|
|
208
|
+
const task = record.task;
|
|
209
|
+
if (isRecord(task) && typeof task.id === "string" && isRecord(task.status)) {
|
|
210
|
+
return task;
|
|
211
|
+
}
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
function isTerminalTaskState(state) {
|
|
215
|
+
return [
|
|
216
|
+
"TASK_STATE_COMPLETED",
|
|
217
|
+
"TASK_STATE_CANCELED",
|
|
218
|
+
"TASK_STATE_FAILED",
|
|
219
|
+
"TASK_STATE_REJECTED",
|
|
220
|
+
].includes(state);
|
|
221
|
+
}
|
|
107
222
|
function normalizeHeaders(headers) {
|
|
108
223
|
const normalized = {};
|
|
109
224
|
for (const [name, value] of Object.entries(headers)) {
|
|
@@ -141,6 +256,10 @@ function requestPathname(url = "/") {
|
|
|
141
256
|
return "/";
|
|
142
257
|
}
|
|
143
258
|
}
|
|
259
|
+
function isSubscribeTaskPath(pathname) {
|
|
260
|
+
const parts = pathname.split("/").filter(Boolean);
|
|
261
|
+
return parts.length === 2 && parts[0] === "tasks" && (parts[1] ?? "").endsWith(A2A_HTTP_SUBSCRIBE_TASK_SUFFIX);
|
|
262
|
+
}
|
|
144
263
|
function isAddressInfo(value) {
|
|
145
264
|
return Boolean(value)
|
|
146
265
|
&& typeof value === "object"
|
|
@@ -148,6 +267,9 @@ function isAddressInfo(value) {
|
|
|
148
267
|
&& "address" in value
|
|
149
268
|
&& "port" in value;
|
|
150
269
|
}
|
|
270
|
+
function isRecord(value) {
|
|
271
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
272
|
+
}
|
|
151
273
|
class BodyTooLargeError extends Error {
|
|
152
274
|
constructor() {
|
|
153
275
|
super("A2A request body is too large.");
|
package/dist/a2aPush.d.ts
CHANGED
|
@@ -30,9 +30,12 @@ export interface A2APushNotificationTransportResponse {
|
|
|
30
30
|
readonly status: number;
|
|
31
31
|
}
|
|
32
32
|
export type A2APushNotificationTransport = (request: A2APushNotificationDeliveryRequest) => A2APushNotificationTransportResponse | Promise<A2APushNotificationTransportResponse>;
|
|
33
|
+
export type A2ACallbackHostResolver = (hostname: string) => readonly string[] | Promise<readonly string[]>;
|
|
33
34
|
export interface A2APushHttpTransportOptions {
|
|
34
35
|
readonly allowedCallbackHosts?: readonly string[];
|
|
35
36
|
readonly fetch?: typeof fetch;
|
|
37
|
+
readonly requireCallbackHostAllowlist?: boolean;
|
|
38
|
+
readonly resolveCallbackHost?: A2ACallbackHostResolver;
|
|
36
39
|
readonly timeoutMs?: number;
|
|
37
40
|
}
|
|
38
41
|
export interface A2APushNotificationDeliveryAttempt {
|
package/dist/a2aPush.js
CHANGED
|
@@ -327,11 +327,17 @@ export function createA2APushHttpTransport(options = {}) {
|
|
|
327
327
|
if (typeof fetchImpl !== "function") {
|
|
328
328
|
throw new A2APushNotificationError("A2A_PUSH_CONFIG_INVALID", "A2A push HTTP transport requires fetch support.");
|
|
329
329
|
}
|
|
330
|
+
if (options.requireCallbackHostAllowlist && (!options.allowedCallbackHosts || options.allowedCallbackHosts.length === 0)) {
|
|
331
|
+
throw new A2APushNotificationError("A2A_PUSH_CONFIG_INVALID", "A2A production push HTTP transport requires a callback host allowlist.");
|
|
332
|
+
}
|
|
330
333
|
const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
|
|
331
334
|
return async (request) => {
|
|
332
335
|
const url = safeWebhookUrl(request.url, {
|
|
333
336
|
allowedCallbackHosts: options.allowedCallbackHosts,
|
|
334
337
|
});
|
|
338
|
+
if (options.resolveCallbackHost) {
|
|
339
|
+
await assertResolvedWebhookHostSafe(new URL(url).hostname, options.resolveCallbackHost);
|
|
340
|
+
}
|
|
335
341
|
const controller = new AbortController();
|
|
336
342
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
337
343
|
try {
|
|
@@ -349,6 +355,18 @@ export function createA2APushHttpTransport(options = {}) {
|
|
|
349
355
|
}
|
|
350
356
|
};
|
|
351
357
|
}
|
|
358
|
+
async function assertResolvedWebhookHostSafe(hostname, resolver) {
|
|
359
|
+
let addresses;
|
|
360
|
+
try {
|
|
361
|
+
addresses = await resolver(hostname);
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
throw new A2APushNotificationError("A2A_PUSH_URL_UNSAFE", "A2A push notification URL host could not be resolved safely.");
|
|
365
|
+
}
|
|
366
|
+
if (addresses.length === 0 || addresses.some((address) => isUnsafeWebhookHost(address))) {
|
|
367
|
+
throw new A2APushNotificationError("A2A_PUSH_URL_UNSAFE", "A2A push notification URL host resolves to an unsafe network address.");
|
|
368
|
+
}
|
|
369
|
+
}
|
|
352
370
|
function parsePushNotificationConfig(taskId, value, options = {}) {
|
|
353
371
|
if (!isRecord(value)) {
|
|
354
372
|
throw new A2APushNotificationError("A2A_PUSH_CONFIG_INVALID", "A2A push notification config must be an object.");
|
|
@@ -586,6 +604,9 @@ function isUnsafeWebhookHost(hostname) {
|
|
|
586
604
|
|| first >= 224;
|
|
587
605
|
}
|
|
588
606
|
if (ipVersion === 6) {
|
|
607
|
+
if (normalized.startsWith("::ffff:")) {
|
|
608
|
+
return isUnsafeWebhookHost(normalized.slice("::ffff:".length));
|
|
609
|
+
}
|
|
589
610
|
return normalized === "::"
|
|
590
611
|
|| normalized === "::1"
|
|
591
612
|
|| normalized.startsWith("fc")
|
package/dist/a2aTask.d.ts
CHANGED
|
@@ -7,6 +7,10 @@ export type A2ATaskState = "TASK_STATE_SUBMITTED" | "TASK_STATE_WORKING" | "TASK
|
|
|
7
7
|
export interface A2APart {
|
|
8
8
|
readonly text?: string;
|
|
9
9
|
readonly data?: Record<string, unknown>;
|
|
10
|
+
readonly raw?: string;
|
|
11
|
+
readonly url?: string;
|
|
12
|
+
readonly filename?: string;
|
|
13
|
+
readonly mediaType?: string;
|
|
10
14
|
readonly file?: {
|
|
11
15
|
readonly uri?: string;
|
|
12
16
|
readonly bytes?: string;
|
|
@@ -54,11 +58,11 @@ export interface A2ATask {
|
|
|
54
58
|
};
|
|
55
59
|
readonly metadata?: Record<string, unknown>;
|
|
56
60
|
}
|
|
57
|
-
export type A2ATaskErrorCode = "A2A_VERSION_NOT_SUPPORTED" | "A2A_MESSAGE_INVALID" | "A2A_MANIFEST_REQUIRED" | "A2A_POLICY_REQUIRED" | "A2A_MANIFEST_INVALID" | "A2A_TASK_NOT_FOUND" | "A2A_TASK_TERMINAL" | "A2A_CONTEXT_MISMATCH";
|
|
61
|
+
export type A2ATaskErrorCode = "A2A_VERSION_NOT_SUPPORTED" | "A2A_MESSAGE_INVALID" | "A2A_CONTENT_TYPE_NOT_SUPPORTED" | "A2A_MANIFEST_REQUIRED" | "A2A_POLICY_REQUIRED" | "A2A_MANIFEST_INVALID" | "A2A_TASK_NOT_FOUND" | "A2A_TASK_TERMINAL" | "A2A_CONTEXT_MISMATCH";
|
|
58
62
|
export declare class A2ATaskError extends Error {
|
|
59
63
|
readonly code: A2ATaskErrorCode;
|
|
60
|
-
readonly status: 400 | 404 | 409;
|
|
61
|
-
constructor(code: A2ATaskErrorCode, message: string, status?: 400 | 404 | 409);
|
|
64
|
+
readonly status: 400 | 404 | 409 | 415;
|
|
65
|
+
constructor(code: A2ATaskErrorCode, message: string, status?: 400 | 404 | 409 | 415);
|
|
62
66
|
}
|
|
63
67
|
export interface A2AProcessMessageContext {
|
|
64
68
|
readonly task: A2ATask;
|
|
@@ -71,6 +75,7 @@ export interface A2AProcessMessageResult {
|
|
|
71
75
|
readonly message?: A2AMessage;
|
|
72
76
|
readonly artifacts?: readonly A2AArtifact[];
|
|
73
77
|
readonly metadata?: Record<string, unknown>;
|
|
78
|
+
readonly responseKind?: "task" | "message";
|
|
74
79
|
}
|
|
75
80
|
export interface SendA2AMessageOptions {
|
|
76
81
|
readonly store: LocalA2ATaskStore;
|
|
@@ -80,10 +85,13 @@ export interface SendA2AMessageOptions {
|
|
|
80
85
|
readonly policy?: AgentActionPolicy;
|
|
81
86
|
readonly now?: Date;
|
|
82
87
|
readonly contextId?: string;
|
|
88
|
+
readonly allowUnmanifestedTasks?: boolean;
|
|
89
|
+
readonly returnImmediately?: boolean;
|
|
83
90
|
readonly processMessage?: (context: A2AProcessMessageContext) => Promise<A2AProcessMessageResult> | A2AProcessMessageResult;
|
|
84
91
|
}
|
|
85
92
|
export interface SendA2AMessageResult {
|
|
86
93
|
readonly task: A2ATask;
|
|
94
|
+
readonly message?: A2AMessage;
|
|
87
95
|
readonly policyDecision?: AgentPolicyDecision;
|
|
88
96
|
}
|
|
89
97
|
export interface GetA2ATaskOptions {
|
package/dist/a2aTask.js
CHANGED
|
@@ -78,6 +78,21 @@ export function redactA2ATaskForLog(task) {
|
|
|
78
78
|
}
|
|
79
79
|
function createTask(options, message, now) {
|
|
80
80
|
if (options.manifest === undefined) {
|
|
81
|
+
if (options.allowUnmanifestedTasks) {
|
|
82
|
+
const baseTask = {
|
|
83
|
+
id: randomId("task"),
|
|
84
|
+
contextId: message.contextId ?? options.contextId ?? randomId("ctx"),
|
|
85
|
+
status: {
|
|
86
|
+
state: "TASK_STATE_SUBMITTED",
|
|
87
|
+
timestamp: timestamp(now),
|
|
88
|
+
},
|
|
89
|
+
history: [message],
|
|
90
|
+
};
|
|
91
|
+
if (options.returnImmediately) {
|
|
92
|
+
return { task: options.store.put(baseTask) };
|
|
93
|
+
}
|
|
94
|
+
return processAndStoreTask(options, baseTask, message, now);
|
|
95
|
+
}
|
|
81
96
|
throw new A2ATaskError("A2A_MANIFEST_REQUIRED", "New A2A tasks require an Vallum manifest.");
|
|
82
97
|
}
|
|
83
98
|
if (!options.policy) {
|
|
@@ -115,6 +130,9 @@ function createTask(options, message, now) {
|
|
|
115
130
|
if (!policyDecision.allowed) {
|
|
116
131
|
return { task: options.store.put(baseTask), policyDecision };
|
|
117
132
|
}
|
|
133
|
+
if (options.returnImmediately) {
|
|
134
|
+
return { task: options.store.put(baseTask), policyDecision };
|
|
135
|
+
}
|
|
118
136
|
return processAndStoreTask(options, baseTask, message, now, manifest, policyDecision);
|
|
119
137
|
}
|
|
120
138
|
async function continueTask(options, message, now) {
|
|
@@ -151,6 +169,9 @@ async function processAndStoreTask(options, task, message, now, manifest, policy
|
|
|
151
169
|
assertValidTaskState(outcome.state);
|
|
152
170
|
if (outcome.message)
|
|
153
171
|
assertValidMessage(outcome.message, "$.processMessage.message");
|
|
172
|
+
if (outcome.responseKind === "message" && !outcome.message) {
|
|
173
|
+
throw new A2ATaskError("A2A_MESSAGE_INVALID", "A2A message responses require an agent message.");
|
|
174
|
+
}
|
|
154
175
|
for (const [index, artifact] of (outcome.artifacts ?? []).entries()) {
|
|
155
176
|
assertValidArtifact(artifact, `$.processMessage.artifacts[${index}]`);
|
|
156
177
|
}
|
|
@@ -169,7 +190,11 @@ async function processAndStoreTask(options, task, message, now, manifest, policy
|
|
|
169
190
|
...(outcome.artifacts ? { artifacts: outcome.artifacts.map(redactA2AArtifact) } : {}),
|
|
170
191
|
...(outcome.metadata ? { metadata: redactRecord(outcome.metadata) } : {}),
|
|
171
192
|
};
|
|
172
|
-
return {
|
|
193
|
+
return {
|
|
194
|
+
task: options.store.put(nextTask),
|
|
195
|
+
...(outcome.responseKind === "message" && statusMessage ? { message: statusMessage } : {}),
|
|
196
|
+
policyDecision,
|
|
197
|
+
};
|
|
173
198
|
}
|
|
174
199
|
function rejectionMessage(policyDecision, now) {
|
|
175
200
|
return {
|
|
@@ -233,10 +258,21 @@ function assertValidPart(part, path) {
|
|
|
233
258
|
typeof part.text === "string" && part.text.trim() !== "",
|
|
234
259
|
isRecord(part.data),
|
|
235
260
|
isRecord(part.file),
|
|
261
|
+
typeof part.raw === "string" && part.raw.trim() !== "",
|
|
262
|
+
typeof part.url === "string" && part.url.trim() !== "",
|
|
236
263
|
].filter(Boolean);
|
|
237
264
|
if (variants.length !== 1) {
|
|
238
265
|
throw new A2ATaskError("A2A_MESSAGE_INVALID", `${path} must contain exactly one text, data, or file part.`);
|
|
239
266
|
}
|
|
267
|
+
if ((typeof part.raw === "string" || typeof part.url === "string") && !isSupportedFileMediaType(part.mediaType)) {
|
|
268
|
+
throw new A2ATaskError("A2A_CONTENT_TYPE_NOT_SUPPORTED", "A2A message part media type is not supported.", 415);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function isSupportedFileMediaType(mediaType) {
|
|
272
|
+
return typeof mediaType === "string" && [
|
|
273
|
+
"text/plain",
|
|
274
|
+
"application/json",
|
|
275
|
+
].includes(mediaType.trim().toLowerCase());
|
|
240
276
|
}
|
|
241
277
|
function taskView(task, options) {
|
|
242
278
|
const history = typeof options.historyLength === "number"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vallum/standards",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.1-prerelease.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
},
|
|
13
13
|
"license": "Apache-2.0",
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@vallum/manifest": "0.0.
|
|
16
|
-
"@vallum/policy-gateway": "0.0.
|
|
17
|
-
"@vallum/receipts": "0.0.
|
|
18
|
-
"@vallum/registry": "0.0.
|
|
15
|
+
"@vallum/manifest": "0.0.1-prerelease.0",
|
|
16
|
+
"@vallum/policy-gateway": "0.0.1-prerelease.0",
|
|
17
|
+
"@vallum/receipts": "0.0.1-prerelease.0",
|
|
18
|
+
"@vallum/registry": "0.0.1-prerelease.0"
|
|
19
19
|
},
|
|
20
20
|
"description": "Standards bridge adapters for Vallum.",
|
|
21
21
|
"files": [
|