@vallum/standards 0.0.0-prerelease
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/README.md +56 -0
- package/dist/a2a.d.ts +1 -0
- package/dist/a2a.js +1 -0
- package/dist/a2aHttp.d.ts +63 -0
- package/dist/a2aHttp.js +338 -0
- package/dist/a2aNodeServer.d.ts +17 -0
- package/dist/a2aNodeServer.js +156 -0
- package/dist/a2aPush.d.ts +162 -0
- package/dist/a2aPush.js +608 -0
- package/dist/a2aTask.d.ts +125 -0
- package/dist/a2aTask.js +327 -0
- package/dist/ap2.d.ts +38 -0
- package/dist/ap2.js +56 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/x402.d.ts +62 -0
- package/dist/x402.js +76 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @vallum/standards
|
|
2
|
+
|
|
3
|
+
Standards bridge adapters for Vallum.
|
|
4
|
+
|
|
5
|
+
Current surface:
|
|
6
|
+
|
|
7
|
+
- x402 v2 payment requirement to Agent Transaction Manifest mapping.
|
|
8
|
+
- Local mock x402 facilitator flow wired through the Vallum policy
|
|
9
|
+
gateway evaluator.
|
|
10
|
+
- x402 external payment receipt linkage and log-safe payment metadata redaction.
|
|
11
|
+
- AP2 checkout/payment mandate to Agent Transaction Manifest mapping.
|
|
12
|
+
- Local mock AP2 mandate flow wired through the Vallum policy gateway
|
|
13
|
+
evaluator.
|
|
14
|
+
- AP2 receipt evidence linkage, dispute evidence pointers, and log-safe
|
|
15
|
+
mandate metadata redaction.
|
|
16
|
+
- A2A Agent Card mapping from Vallum Agent Profiles using current
|
|
17
|
+
discovery fields, auth declarations, supported interfaces, modes, and skills.
|
|
18
|
+
- A2A public-card hardening that fails closed for revoked/expired profiles,
|
|
19
|
+
unsupported local protocol versions, malformed auth declarations, and private
|
|
20
|
+
profile fields in public metadata.
|
|
21
|
+
- A2A Agent Card JWS signing and trusted-key verification helpers for local
|
|
22
|
+
signed-card proof.
|
|
23
|
+
- A2A well-known response helpers for serving local Agent Cards at the
|
|
24
|
+
canonical `/.well-known/agent-card.json` path.
|
|
25
|
+
- A2A JWKS response helpers for serving explicitly configured public signing
|
|
26
|
+
keys at the canonical `/.well-known/jwks.json` path without private key
|
|
27
|
+
material.
|
|
28
|
+
- Local/mock A2A task and message operation helpers for send-message,
|
|
29
|
+
get-task, list-tasks, and cancel-task semantics, gated by Vallum
|
|
30
|
+
manifest/policy metadata and log-safe redaction.
|
|
31
|
+
- Local HTTP-shaped A2A handler for public Agent Card discovery and
|
|
32
|
+
bearer-authenticated task send/get/list/cancel routes.
|
|
33
|
+
- Authenticated local A2A extended Agent Card access through the HTTP-shaped
|
|
34
|
+
handler when an extended card is explicitly configured.
|
|
35
|
+
- Local A2A push notification configuration CRUD that accepts public HTTPS
|
|
36
|
+
callback URLs while rejecting webhook credential storage and unsafe local
|
|
37
|
+
destinations.
|
|
38
|
+
- Local injected A2A push delivery envelopes that POST sanitized task payloads
|
|
39
|
+
through an explicit transport without default outbound webhook calls.
|
|
40
|
+
- Opt-in A2A push HTTP transport helper that posts sanitized task payloads only
|
|
41
|
+
when explicitly injected, rejects unsafe callback URLs before network
|
|
42
|
+
contact, does not emit authorization headers, uses manual redirect handling,
|
|
43
|
+
applies a timeout, and returns status-only results.
|
|
44
|
+
- Local A2A push retry and in-memory attempt observability for explicitly
|
|
45
|
+
injected transports, recording status-only attempt metadata without request
|
|
46
|
+
bodies or credential material.
|
|
47
|
+
- Loopback-only Node HTTP server helper for deterministic local A2A discovery
|
|
48
|
+
and task route smoke proof, including optional local JWKS serving, with
|
|
49
|
+
explicit unsafe opt-in required for non-loopback binds.
|
|
50
|
+
|
|
51
|
+
This package does not operate a production x402 facilitator, replace AP2, hold
|
|
52
|
+
payment credentials, sign payment payloads, submit live settlement
|
|
53
|
+
transactions, operate a live public A2A task/message server, publish public A2A
|
|
54
|
+
discovery, prove public A2A push/webhook infrastructure, operate production
|
|
55
|
+
push queues/workers, prove external A2A conformance or live A2A discovery,
|
|
56
|
+
provide production Agent Card key management, or replace the A2A protocol.
|
package/dist/a2a.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { A2A_AGENT_CARD_MEDIA_TYPE, A2A_AGENT_CARD_PROTOCOL_VERSION, A2A_AGENT_CARD_WELL_KNOWN_PATH, AGENTIC_VALLUM_A2A_PROFILE_EXTENSION_URI, A2AAgentCardError, createA2AAgentCardWellKnownResponse, createA2AAgentCardFromProfile, canonicalizeA2AAgentCard, handleA2AAgentCardWellKnownRequest, signA2AAgentCard, verifyA2AAgentCardSignature, type A2AAgentCapabilities, type A2AAgentCard, type A2AAgentCardErrorCode, type A2AAgentCardSignature, type A2AAgentCardSignatureAlgorithm, type A2AAgentCardSignatureVerification, type A2AAgentCardWellKnownOptions, type A2AAgentCardWellKnownResponse, type A2AAgentExtension, type A2AAgentInterface, type A2AAgentProvider, type A2AAgentSkill, type A2AWellKnownErrorCode, type A2AWellKnownErrorResponse, type A2AWellKnownRequest, type A2AWellKnownResponse, type A2AProtocolBinding, type A2ASecurityRequirement, type A2ASecurityScheme, type CreateA2AAgentCardOptions, type SignA2AAgentCardOptions, type SignedA2AAgentCard, type VerifyA2AAgentCardSignatureOptions, } from "@vallum/registry";
|
package/dist/a2a.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { A2A_AGENT_CARD_MEDIA_TYPE, A2A_AGENT_CARD_PROTOCOL_VERSION, A2A_AGENT_CARD_WELL_KNOWN_PATH, AGENTIC_VALLUM_A2A_PROFILE_EXTENSION_URI, A2AAgentCardError, createA2AAgentCardWellKnownResponse, createA2AAgentCardFromProfile, canonicalizeA2AAgentCard, handleA2AAgentCardWellKnownRequest, signA2AAgentCard, verifyA2AAgentCardSignature, } from "@vallum/registry";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type A2AAgentCardWellKnownOptions } from "@vallum/registry";
|
|
2
|
+
import type { AgentActionPolicy } from "@vallum/policy-gateway";
|
|
3
|
+
import { type A2APushNotificationTransport, type A2ATaskPushNotificationConfig, type LocalA2APushNotificationStore } from "./a2aPush.js";
|
|
4
|
+
import { type A2ATask, type A2AProcessMessageContext, type A2AProcessMessageResult, type LocalA2ATaskStore } from "./a2aTask.js";
|
|
5
|
+
export interface A2AHttpRequest {
|
|
6
|
+
readonly method?: string;
|
|
7
|
+
readonly path?: string;
|
|
8
|
+
readonly headers?: Record<string, string | undefined>;
|
|
9
|
+
readonly body?: unknown;
|
|
10
|
+
}
|
|
11
|
+
export type A2AHttpResponseBody = {
|
|
12
|
+
readonly kind: "agent-card";
|
|
13
|
+
readonly [key: string]: unknown;
|
|
14
|
+
} | {
|
|
15
|
+
readonly kind: "task";
|
|
16
|
+
readonly task: A2ATask;
|
|
17
|
+
readonly policyDecision?: unknown;
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: "task-list";
|
|
20
|
+
readonly tasks: readonly A2ATask[];
|
|
21
|
+
} | {
|
|
22
|
+
readonly kind: "push-config";
|
|
23
|
+
readonly config: A2ATaskPushNotificationConfig;
|
|
24
|
+
} | {
|
|
25
|
+
readonly kind: "push-config-list";
|
|
26
|
+
readonly configs: readonly A2ATaskPushNotificationConfig[];
|
|
27
|
+
readonly nextPageToken?: string;
|
|
28
|
+
} | {
|
|
29
|
+
readonly kind: "push-config-deleted";
|
|
30
|
+
readonly taskId: string;
|
|
31
|
+
readonly id: string;
|
|
32
|
+
readonly deleted: boolean;
|
|
33
|
+
} | {
|
|
34
|
+
readonly error: {
|
|
35
|
+
readonly code: A2AHttpErrorCode | string;
|
|
36
|
+
readonly message: string;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
export interface A2AHttpResponse {
|
|
40
|
+
readonly status: 200 | 400 | 401 | 404 | 405 | 409 | 410 | 415 | 501 | 503;
|
|
41
|
+
readonly headers: Record<string, string>;
|
|
42
|
+
readonly body: A2AHttpResponseBody;
|
|
43
|
+
readonly json: string;
|
|
44
|
+
}
|
|
45
|
+
export type A2AHttpErrorCode = "A2A_AUTH_NOT_CONFIGURED" | "A2A_AUTH_REQUIRED" | "A2A_BODY_INVALID" | "A2A_EXTENDED_AGENT_CARD_NOT_CONFIGURED" | "A2A_ROUTE_NOT_FOUND" | "A2A_METHOD_NOT_ALLOWED" | "A2A_VERSION_NOT_SUPPORTED" | "A2A_OPERATION_UNSUPPORTED" | "A2A_POLICY_NOT_CONFIGURED" | "A2A_INTERNAL_ERROR";
|
|
46
|
+
export interface LocalA2AHttpHandlerOptions {
|
|
47
|
+
readonly store: LocalA2ATaskStore;
|
|
48
|
+
readonly agentCardProfile?: unknown;
|
|
49
|
+
readonly agentCardOptions?: A2AAgentCardWellKnownOptions;
|
|
50
|
+
readonly extendedAgentCardProfile?: unknown;
|
|
51
|
+
readonly extendedAgentCardOptions?: A2AAgentCardWellKnownOptions;
|
|
52
|
+
readonly taskAuthToken?: string;
|
|
53
|
+
readonly taskPolicy?: AgentActionPolicy;
|
|
54
|
+
readonly pushNotificationStore?: LocalA2APushNotificationStore;
|
|
55
|
+
readonly pushNotificationTransport?: A2APushNotificationTransport;
|
|
56
|
+
readonly now?: () => Date;
|
|
57
|
+
readonly processMessage?: (context: A2AProcessMessageContext) => Promise<A2AProcessMessageResult> | A2AProcessMessageResult;
|
|
58
|
+
}
|
|
59
|
+
export declare const A2A_HTTP_SEND_MESSAGE_PATH: "/message:send";
|
|
60
|
+
export declare const A2A_HTTP_STREAM_MESSAGE_PATH: "/message:stream";
|
|
61
|
+
export declare const A2A_HTTP_EXTENDED_AGENT_CARD_PATH: "/extendedAgentCard";
|
|
62
|
+
export declare const A2A_HTTP_TASKS_PATH: "/tasks";
|
|
63
|
+
export declare function handleLocalA2AHttpRequest(request: A2AHttpRequest, options: LocalA2AHttpHandlerOptions): Promise<A2AHttpResponse>;
|
package/dist/a2aHttp.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { A2A_AGENT_CARD_WELL_KNOWN_PATH, handleA2AAgentCardWellKnownRequest, } from "@vallum/registry";
|
|
2
|
+
import { A2APushNotificationError, createA2APushNotificationConfig, deleteA2APushNotificationConfig, deliverA2APushNotifications, getA2APushNotificationConfig, listA2APushNotificationConfigs, } from "./a2aPush.js";
|
|
3
|
+
import { A2A_TASK_MEDIA_TYPE, A2A_TASK_PROTOCOL_VERSION, A2ATaskError, cancelA2ATask, getA2ATask, listA2ATasks, sendA2AMessage, } from "./a2aTask.js";
|
|
4
|
+
export const A2A_HTTP_SEND_MESSAGE_PATH = "/message:send";
|
|
5
|
+
export const A2A_HTTP_STREAM_MESSAGE_PATH = "/message:stream";
|
|
6
|
+
export const A2A_HTTP_EXTENDED_AGENT_CARD_PATH = "/extendedAgentCard";
|
|
7
|
+
export const A2A_HTTP_TASKS_PATH = "/tasks";
|
|
8
|
+
export async function handleLocalA2AHttpRequest(request, options) {
|
|
9
|
+
const method = normalizeMethod(request.method);
|
|
10
|
+
const url = parsePath(request.path);
|
|
11
|
+
if (url.pathname === A2A_AGENT_CARD_WELL_KNOWN_PATH) {
|
|
12
|
+
return handleAgentCardRequest(method, url.pathname, options.agentCardProfile, {
|
|
13
|
+
...publicAgentCardOptions(options),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
const auth = authorizeTaskRequest(request, options);
|
|
17
|
+
if (auth)
|
|
18
|
+
return auth;
|
|
19
|
+
const versionError = validateProtocolVersion(request);
|
|
20
|
+
if (versionError)
|
|
21
|
+
return versionError;
|
|
22
|
+
if (url.pathname === A2A_HTTP_STREAM_MESSAGE_PATH) {
|
|
23
|
+
return errorResponse(501, "A2A_OPERATION_UNSUPPORTED", "A2A streaming is supported by the local Node SSE server, not by this pure HTTP response handler.");
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
if (url.pathname === A2A_HTTP_EXTENDED_AGENT_CARD_PATH) {
|
|
27
|
+
if (method !== "GET")
|
|
28
|
+
return methodNotAllowed(["GET"]);
|
|
29
|
+
return handleExtendedAgentCardRequest(options);
|
|
30
|
+
}
|
|
31
|
+
const pushRoute = matchPushNotificationRoute(url.pathname);
|
|
32
|
+
if (pushRoute)
|
|
33
|
+
return handlePushNotificationRoute(method, url, request, options, pushRoute);
|
|
34
|
+
if (url.pathname === A2A_HTTP_SEND_MESSAGE_PATH) {
|
|
35
|
+
if (method !== "POST")
|
|
36
|
+
return methodNotAllowed(["POST"]);
|
|
37
|
+
return await handleSendMessage(request, options);
|
|
38
|
+
}
|
|
39
|
+
if (url.pathname === A2A_HTTP_TASKS_PATH) {
|
|
40
|
+
if (method !== "GET")
|
|
41
|
+
return methodNotAllowed(["GET"]);
|
|
42
|
+
return ok({
|
|
43
|
+
kind: "task-list",
|
|
44
|
+
...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: numberQuery(url, "pageSize"),
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const taskRoute = matchTaskRoute(url.pathname);
|
|
55
|
+
if (taskRoute) {
|
|
56
|
+
if (taskRoute.action === "get") {
|
|
57
|
+
if (method !== "GET")
|
|
58
|
+
return methodNotAllowed(["GET"]);
|
|
59
|
+
return ok({
|
|
60
|
+
kind: "task",
|
|
61
|
+
...getA2ATask({
|
|
62
|
+
store: options.store,
|
|
63
|
+
id: taskRoute.taskId,
|
|
64
|
+
includeArtifacts: booleanQuery(url, "includeArtifacts"),
|
|
65
|
+
historyLength: numberQuery(url, "historyLength"),
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (method !== "POST")
|
|
70
|
+
return methodNotAllowed(["POST"]);
|
|
71
|
+
const result = cancelA2ATask({
|
|
72
|
+
store: options.store,
|
|
73
|
+
id: taskRoute.taskId,
|
|
74
|
+
now: options.now?.(),
|
|
75
|
+
includeArtifacts: booleanQuery(url, "includeArtifacts"),
|
|
76
|
+
});
|
|
77
|
+
await maybeDeliverPushNotifications(result.task, options);
|
|
78
|
+
return ok({
|
|
79
|
+
kind: "task",
|
|
80
|
+
...result,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return errorResponse(404, "A2A_ROUTE_NOT_FOUND", "A2A route was not found.");
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error instanceof A2ATaskError) {
|
|
87
|
+
return errorResponse(error.status, error.code, error.message);
|
|
88
|
+
}
|
|
89
|
+
if (error instanceof A2APushNotificationError) {
|
|
90
|
+
return errorResponse(error.status, error.code, error.message);
|
|
91
|
+
}
|
|
92
|
+
return errorResponse(400, "A2A_BODY_INVALID", "A2A request body is invalid.");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function publicAgentCardOptions(options) {
|
|
96
|
+
const base = {
|
|
97
|
+
...options.agentCardOptions,
|
|
98
|
+
now: options.agentCardOptions?.now ?? options.now?.(),
|
|
99
|
+
};
|
|
100
|
+
if (options.extendedAgentCardProfile === undefined)
|
|
101
|
+
return base;
|
|
102
|
+
return {
|
|
103
|
+
...base,
|
|
104
|
+
capabilities: {
|
|
105
|
+
...base.capabilities,
|
|
106
|
+
extendedAgentCard: true,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function handleAgentCardRequest(method, path, profile, options = {}) {
|
|
111
|
+
const response = handleA2AAgentCardWellKnownRequest({ method, path }, profile, options);
|
|
112
|
+
if (response.status === 200) {
|
|
113
|
+
return {
|
|
114
|
+
status: 200,
|
|
115
|
+
headers: response.headers,
|
|
116
|
+
body: {
|
|
117
|
+
kind: "agent-card",
|
|
118
|
+
...response.body,
|
|
119
|
+
},
|
|
120
|
+
json: response.json,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
status: response.status,
|
|
125
|
+
headers: response.headers,
|
|
126
|
+
body: JSON.parse(response.json),
|
|
127
|
+
json: response.json,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function handleExtendedAgentCardRequest(options) {
|
|
131
|
+
if (options.extendedAgentCardProfile === undefined) {
|
|
132
|
+
return errorResponse(501, "A2A_EXTENDED_AGENT_CARD_NOT_CONFIGURED", "A2A extended Agent Card is not configured for this local Vallum server.");
|
|
133
|
+
}
|
|
134
|
+
return handleAgentCardRequest("GET", A2A_AGENT_CARD_WELL_KNOWN_PATH, options.extendedAgentCardProfile, {
|
|
135
|
+
...options.extendedAgentCardOptions,
|
|
136
|
+
now: options.extendedAgentCardOptions?.now ?? options.now?.(),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
async function handleSendMessage(request, options) {
|
|
140
|
+
if (!options.taskPolicy) {
|
|
141
|
+
return errorResponse(503, "A2A_POLICY_NOT_CONFIGURED", "A2A task endpoint policy is not configured.");
|
|
142
|
+
}
|
|
143
|
+
const body = parseBody(request.body);
|
|
144
|
+
if (!isSendMessageBody(body)) {
|
|
145
|
+
return errorResponse(400, "A2A_BODY_INVALID", "A2A request body is invalid.");
|
|
146
|
+
}
|
|
147
|
+
const result = await sendA2AMessage({
|
|
148
|
+
store: options.store,
|
|
149
|
+
protocolVersion: body.protocolVersion ?? A2A_TASK_PROTOCOL_VERSION,
|
|
150
|
+
message: body.message,
|
|
151
|
+
manifest: body.manifest,
|
|
152
|
+
policy: body.message.taskId ? undefined : options.taskPolicy,
|
|
153
|
+
now: options.now?.(),
|
|
154
|
+
contextId: body.contextId,
|
|
155
|
+
processMessage: options.processMessage,
|
|
156
|
+
});
|
|
157
|
+
await maybeDeliverPushNotifications(result.task, options);
|
|
158
|
+
return ok({
|
|
159
|
+
kind: "task",
|
|
160
|
+
...result,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function handlePushNotificationRoute(method, url, request, options, route) {
|
|
164
|
+
if (!options.pushNotificationStore) {
|
|
165
|
+
return errorResponse(501, "A2A_OPERATION_UNSUPPORTED", "A2A push notification configuration is not enabled for this local Vallum server.");
|
|
166
|
+
}
|
|
167
|
+
getA2ATask({ store: options.store, id: route.taskId });
|
|
168
|
+
if (!route.configId) {
|
|
169
|
+
if (method === "POST") {
|
|
170
|
+
return ok({
|
|
171
|
+
kind: "push-config",
|
|
172
|
+
config: createA2APushNotificationConfig({
|
|
173
|
+
store: options.pushNotificationStore,
|
|
174
|
+
taskId: route.taskId,
|
|
175
|
+
value: parseBody(request.body),
|
|
176
|
+
now: options.now?.(),
|
|
177
|
+
}),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (method === "GET") {
|
|
181
|
+
return ok({
|
|
182
|
+
kind: "push-config-list",
|
|
183
|
+
...listA2APushNotificationConfigs({
|
|
184
|
+
store: options.pushNotificationStore,
|
|
185
|
+
taskId: route.taskId,
|
|
186
|
+
pageSize: numberQuery(url, "pageSize"),
|
|
187
|
+
}),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return methodNotAllowed(["GET", "POST"]);
|
|
191
|
+
}
|
|
192
|
+
if (method === "GET") {
|
|
193
|
+
return ok({
|
|
194
|
+
kind: "push-config",
|
|
195
|
+
config: getA2APushNotificationConfig({
|
|
196
|
+
store: options.pushNotificationStore,
|
|
197
|
+
taskId: route.taskId,
|
|
198
|
+
id: route.configId,
|
|
199
|
+
}),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
if (method === "DELETE") {
|
|
203
|
+
return ok({
|
|
204
|
+
kind: "push-config-deleted",
|
|
205
|
+
...deleteA2APushNotificationConfig({
|
|
206
|
+
store: options.pushNotificationStore,
|
|
207
|
+
taskId: route.taskId,
|
|
208
|
+
id: route.configId,
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return methodNotAllowed(["DELETE", "GET"]);
|
|
213
|
+
}
|
|
214
|
+
async function maybeDeliverPushNotifications(task, options) {
|
|
215
|
+
if (!options.pushNotificationStore || !options.pushNotificationTransport)
|
|
216
|
+
return;
|
|
217
|
+
await deliverA2APushNotifications({
|
|
218
|
+
store: options.pushNotificationStore,
|
|
219
|
+
task,
|
|
220
|
+
transport: options.pushNotificationTransport,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function authorizeTaskRequest(request, options) {
|
|
224
|
+
if (!options.taskAuthToken || options.taskAuthToken.trim() === "") {
|
|
225
|
+
return errorResponse(503, "A2A_AUTH_NOT_CONFIGURED", "A2A task endpoint authentication is not configured.");
|
|
226
|
+
}
|
|
227
|
+
const authorization = header(request.headers, "authorization");
|
|
228
|
+
if (authorization !== `Bearer ${options.taskAuthToken}`) {
|
|
229
|
+
return errorResponse(401, "A2A_AUTH_REQUIRED", "A2A task endpoints require bearer authentication.");
|
|
230
|
+
}
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
function validateProtocolVersion(request) {
|
|
234
|
+
const version = header(request.headers, "a2a-version");
|
|
235
|
+
if (version !== undefined && version !== A2A_TASK_PROTOCOL_VERSION) {
|
|
236
|
+
return errorResponse(400, "A2A_VERSION_NOT_SUPPORTED", "A2A protocol version is unsupported.");
|
|
237
|
+
}
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
function ok(body) {
|
|
241
|
+
return {
|
|
242
|
+
status: 200,
|
|
243
|
+
headers: {
|
|
244
|
+
"content-type": `${A2A_TASK_MEDIA_TYPE}; charset=utf-8`,
|
|
245
|
+
"cache-control": "no-store",
|
|
246
|
+
},
|
|
247
|
+
body,
|
|
248
|
+
json: `${JSON.stringify(body)}\n`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function errorResponse(status, code, message, headers = {}) {
|
|
252
|
+
const body = { error: { code, message } };
|
|
253
|
+
return {
|
|
254
|
+
status,
|
|
255
|
+
headers: {
|
|
256
|
+
"content-type": "application/json; charset=utf-8",
|
|
257
|
+
"cache-control": "no-store",
|
|
258
|
+
...headers,
|
|
259
|
+
},
|
|
260
|
+
body,
|
|
261
|
+
json: `${JSON.stringify(body)}\n`,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function methodNotAllowed(allowed) {
|
|
265
|
+
return errorResponse(405, "A2A_METHOD_NOT_ALLOWED", "A2A route does not support this method.", { allow: allowed.join(", ") });
|
|
266
|
+
}
|
|
267
|
+
function parseBody(body) {
|
|
268
|
+
if (typeof body !== "string")
|
|
269
|
+
return body;
|
|
270
|
+
try {
|
|
271
|
+
return JSON.parse(body);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
throw new A2ATaskError("A2A_MESSAGE_INVALID", "A2A request body is invalid.");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function isSendMessageBody(value) {
|
|
278
|
+
if (!isRecord(value))
|
|
279
|
+
return false;
|
|
280
|
+
return isRecord(value.message);
|
|
281
|
+
}
|
|
282
|
+
function matchTaskRoute(pathname) {
|
|
283
|
+
const parts = pathname.split("/").filter(Boolean);
|
|
284
|
+
if (parts.length !== 2 || parts[0] !== "tasks")
|
|
285
|
+
return undefined;
|
|
286
|
+
const taskPart = parts[1] ?? "";
|
|
287
|
+
if (taskPart.endsWith(":cancel")) {
|
|
288
|
+
return { taskId: decodeURIComponent(taskPart.slice(0, -":cancel".length)), action: "cancel" };
|
|
289
|
+
}
|
|
290
|
+
return { taskId: decodeURIComponent(taskPart), action: "get" };
|
|
291
|
+
}
|
|
292
|
+
function matchPushNotificationRoute(pathname) {
|
|
293
|
+
const parts = pathname.split("/").filter(Boolean);
|
|
294
|
+
if (parts.length !== 3 && parts.length !== 4)
|
|
295
|
+
return undefined;
|
|
296
|
+
if (parts[0] !== "tasks" || parts[2] !== "pushNotificationConfigs")
|
|
297
|
+
return undefined;
|
|
298
|
+
return {
|
|
299
|
+
taskId: decodeURIComponent(parts[1] ?? ""),
|
|
300
|
+
...(parts[3] ? { configId: decodeURIComponent(parts[3]) } : {}),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function parsePath(path = "/") {
|
|
304
|
+
try {
|
|
305
|
+
return new URL(path, "https://agent.local");
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return new URL("/", "https://agent.local");
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function normalizeMethod(method = "GET") {
|
|
312
|
+
return method.trim().toUpperCase();
|
|
313
|
+
}
|
|
314
|
+
function header(headers, name) {
|
|
315
|
+
const found = Object.entries(headers ?? {})
|
|
316
|
+
.find(([key]) => key.toLowerCase() === name.toLowerCase());
|
|
317
|
+
return found?.[1];
|
|
318
|
+
}
|
|
319
|
+
function booleanQuery(url, name) {
|
|
320
|
+
const value = url.searchParams.get(name);
|
|
321
|
+
if (value === null)
|
|
322
|
+
return undefined;
|
|
323
|
+
return value === "true";
|
|
324
|
+
}
|
|
325
|
+
function numberQuery(url, name) {
|
|
326
|
+
const value = url.searchParams.get(name);
|
|
327
|
+
if (value === null || value.trim() === "")
|
|
328
|
+
return undefined;
|
|
329
|
+
const number = Number(value);
|
|
330
|
+
return Number.isFinite(number) ? number : undefined;
|
|
331
|
+
}
|
|
332
|
+
function optionalQuery(url, name) {
|
|
333
|
+
const value = url.searchParams.get(name);
|
|
334
|
+
return value === null || value.trim() === "" ? undefined : value;
|
|
335
|
+
}
|
|
336
|
+
function isRecord(value) {
|
|
337
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
338
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type A2APublicJwksOptions } from "@vallum/registry";
|
|
2
|
+
import { type LocalA2AHttpHandlerOptions } from "./a2aHttp.js";
|
|
3
|
+
export interface LocalA2ANodeServerOptions extends LocalA2AHttpHandlerOptions {
|
|
4
|
+
readonly host?: string;
|
|
5
|
+
readonly port?: number;
|
|
6
|
+
readonly maxBodyBytes?: number;
|
|
7
|
+
readonly allowNonLoopbackHost?: boolean;
|
|
8
|
+
readonly publicJwks?: A2APublicJwksOptions;
|
|
9
|
+
}
|
|
10
|
+
export interface LocalA2ANodeServer {
|
|
11
|
+
readonly baseUrl: string;
|
|
12
|
+
readonly host: string;
|
|
13
|
+
readonly port: number;
|
|
14
|
+
readonly boundToLoopback: boolean;
|
|
15
|
+
readonly close: () => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export declare function startLocalA2ANodeServer(options: LocalA2ANodeServerOptions): Promise<LocalA2ANodeServer>;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
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";
|
|
4
|
+
const DEFAULT_HOST = "127.0.0.1";
|
|
5
|
+
const DEFAULT_MAX_BODY_BYTES = 64_000;
|
|
6
|
+
export async function startLocalA2ANodeServer(options) {
|
|
7
|
+
const host = options.host ?? DEFAULT_HOST;
|
|
8
|
+
if (!options.allowNonLoopbackHost && !isLoopbackHost(host)) {
|
|
9
|
+
throw new Error("A2A local Node server refuses non-loopback hosts without explicit opt-in.");
|
|
10
|
+
}
|
|
11
|
+
const server = createServer(async (request, response) => {
|
|
12
|
+
try {
|
|
13
|
+
const body = await readBody(request, options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES);
|
|
14
|
+
if (requestPathname(request.url) === A2A_JWKS_WELL_KNOWN_PATH) {
|
|
15
|
+
const handled = handleA2APublicJwksRequest({
|
|
16
|
+
method: request.method,
|
|
17
|
+
path: request.url,
|
|
18
|
+
}, options.publicJwks ?? { keys: [] });
|
|
19
|
+
writeHandledResponse(response, handled.status, handled.headers, handled.json);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (requestPathname(request.url) === A2A_HTTP_STREAM_MESSAGE_PATH) {
|
|
23
|
+
const handled = await handleLocalA2AHttpRequest({
|
|
24
|
+
method: request.method,
|
|
25
|
+
path: A2A_HTTP_SEND_MESSAGE_PATH,
|
|
26
|
+
headers: normalizeHeaders(request.headers),
|
|
27
|
+
body,
|
|
28
|
+
}, options);
|
|
29
|
+
if (handled.status !== 200) {
|
|
30
|
+
writeHandledResponse(response, handled.status, handled.headers, handled.json);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
writeServerSentEvents(response, [{ event: "task", data: handled.body }]);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const handled = await handleLocalA2AHttpRequest({
|
|
37
|
+
method: request.method,
|
|
38
|
+
path: request.url,
|
|
39
|
+
headers: normalizeHeaders(request.headers),
|
|
40
|
+
body,
|
|
41
|
+
}, options);
|
|
42
|
+
writeHandledResponse(response, handled.status, handled.headers, handled.json);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const status = error instanceof BodyTooLargeError ? 413 : 400;
|
|
46
|
+
writeHandledResponse(response, status, {
|
|
47
|
+
"content-type": "application/json; charset=utf-8",
|
|
48
|
+
"cache-control": "no-store",
|
|
49
|
+
}, `${JSON.stringify({
|
|
50
|
+
error: {
|
|
51
|
+
code: error instanceof BodyTooLargeError ? "A2A_BODY_TOO_LARGE" : "A2A_BODY_INVALID",
|
|
52
|
+
message: error instanceof BodyTooLargeError
|
|
53
|
+
? "A2A request body is too large."
|
|
54
|
+
: "A2A request body is invalid.",
|
|
55
|
+
},
|
|
56
|
+
})}\n`);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
await listen(server, host, options.port ?? 0);
|
|
60
|
+
const address = server.address();
|
|
61
|
+
if (!isAddressInfo(address)) {
|
|
62
|
+
await closeServer(server);
|
|
63
|
+
throw new Error("A2A local Node server did not bind to a TCP address.");
|
|
64
|
+
}
|
|
65
|
+
const addressHost = address.address === "::1" ? "[::1]" : address.address;
|
|
66
|
+
return {
|
|
67
|
+
baseUrl: `http://${addressHost}:${address.port}`,
|
|
68
|
+
host: address.address,
|
|
69
|
+
port: address.port,
|
|
70
|
+
boundToLoopback: isLoopbackHost(address.address),
|
|
71
|
+
close: () => closeServer(server),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function listen(server, host, port) {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
server.once("error", reject);
|
|
77
|
+
server.listen(port, host, () => {
|
|
78
|
+
server.off("error", reject);
|
|
79
|
+
resolve();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function closeServer(server) {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
server.close((error) => error ? reject(error) : resolve());
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function writeHandledResponse(response, status, headers, body) {
|
|
89
|
+
response.statusCode = status;
|
|
90
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
91
|
+
response.setHeader(name, value);
|
|
92
|
+
}
|
|
93
|
+
response.end(body);
|
|
94
|
+
}
|
|
95
|
+
function writeServerSentEvents(response, events) {
|
|
96
|
+
response.statusCode = 200;
|
|
97
|
+
response.setHeader("content-type", "text/event-stream; charset=utf-8");
|
|
98
|
+
response.setHeader("cache-control", "no-store");
|
|
99
|
+
response.setHeader("connection", "keep-alive");
|
|
100
|
+
response.setHeader("x-accel-buffering", "no");
|
|
101
|
+
for (const event of events) {
|
|
102
|
+
response.write(`event: ${event.event}\n`);
|
|
103
|
+
response.write(`data: ${JSON.stringify(event.data)}\n\n`);
|
|
104
|
+
}
|
|
105
|
+
response.end();
|
|
106
|
+
}
|
|
107
|
+
function normalizeHeaders(headers) {
|
|
108
|
+
const normalized = {};
|
|
109
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
110
|
+
normalized[name] = Array.isArray(value) ? value.join(", ") : value;
|
|
111
|
+
}
|
|
112
|
+
return normalized;
|
|
113
|
+
}
|
|
114
|
+
async function readBody(request, maxBodyBytes) {
|
|
115
|
+
if (request.method === "GET" || request.method === "HEAD")
|
|
116
|
+
return undefined;
|
|
117
|
+
const chunks = [];
|
|
118
|
+
let byteLength = 0;
|
|
119
|
+
for await (const chunk of request) {
|
|
120
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
121
|
+
byteLength += buffer.byteLength;
|
|
122
|
+
if (byteLength > maxBodyBytes) {
|
|
123
|
+
throw new BodyTooLargeError();
|
|
124
|
+
}
|
|
125
|
+
chunks.push(buffer);
|
|
126
|
+
}
|
|
127
|
+
if (chunks.length === 0)
|
|
128
|
+
return undefined;
|
|
129
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
130
|
+
return body.trim() === "" ? undefined : body;
|
|
131
|
+
}
|
|
132
|
+
function isLoopbackHost(host) {
|
|
133
|
+
const normalized = host.trim().toLowerCase();
|
|
134
|
+
return normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost";
|
|
135
|
+
}
|
|
136
|
+
function requestPathname(url = "/") {
|
|
137
|
+
try {
|
|
138
|
+
return new URL(url, "http://127.0.0.1").pathname;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return "/";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function isAddressInfo(value) {
|
|
145
|
+
return Boolean(value)
|
|
146
|
+
&& typeof value === "object"
|
|
147
|
+
&& value !== null
|
|
148
|
+
&& "address" in value
|
|
149
|
+
&& "port" in value;
|
|
150
|
+
}
|
|
151
|
+
class BodyTooLargeError extends Error {
|
|
152
|
+
constructor() {
|
|
153
|
+
super("A2A request body is too large.");
|
|
154
|
+
this.name = "BodyTooLargeError";
|
|
155
|
+
}
|
|
156
|
+
}
|