@ap3x/a2a 1.0.0 → 2.0.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 +21 -0
- package/README.md +27 -0
- package/dist/backend.d.ts +89 -0
- package/dist/backend.d.ts.map +1 -0
- package/dist/bindings/grpc.d.ts +121 -0
- package/dist/bindings/grpc.d.ts.map +1 -0
- package/dist/bindings/jsonrpc.d.ts +135 -0
- package/dist/bindings/jsonrpc.d.ts.map +1 -0
- package/dist/bindings/rest.d.ts +75 -0
- package/dist/bindings/rest.d.ts.map +1 -0
- package/dist/chunk-PYM7LV6R.js +1590 -0
- package/dist/client.d.ts +116 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/executor.d.ts +91 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/grpc-5DAU6EYQ.js +591 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +454 -0
- package/dist/model.d.ts +480 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/server.d.ts +60 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/task-store.d.ts +61 -0
- package/dist/task-store.d.ts.map +1 -0
- package/package.json +26 -15
- package/proto/a2a.proto +813 -0
- package/proto/google/api/annotations.proto +6 -0
- package/proto/google/api/client.proto +4 -0
- package/proto/google/api/field_behavior.proto +4 -0
- package/src/__tests__/a2a-auth-expired.test.ts +0 -84
- package/src/__tests__/a2a.test.ts +0 -87
- package/src/__tests__/handler-sendsubscribe-error.test.ts +0 -32
- package/src/__tests__/sendsubscribe-rejected-promise.test.ts +0 -89
- package/src/__tests__/server-apikey-warning.test.ts +0 -71
- package/src/__tests__/server-body-limit.test.ts +0 -54
- package/src/__tests__/server-security-headers.test.ts +0 -112
- package/src/__tests__/server-timing-safe-auth.test.ts +0 -45
- package/src/__tests__/sse-handler-error-propagation.test.ts +0 -69
- package/src/card/generator.ts +0 -18
- package/src/client/index.ts +0 -172
- package/src/errors.ts +0 -21
- package/src/index.ts +0 -15
- package/src/server/handler.ts +0 -90
- package/src/server/index.ts +0 -208
- package/src/server/sse.ts +0 -49
- package/src/swarm/a2a-agent.ts +0 -34
- package/src/types.ts +0 -50
- package/tsconfig.json +0 -20
- package/vitest.config.ts +0 -4
package/src/card/generator.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { AgentCard, GenerateCardOptions } from "../types.js";
|
|
2
|
-
|
|
3
|
-
export function generateAgentCard(opts: GenerateCardOptions): AgentCard {
|
|
4
|
-
return {
|
|
5
|
-
name: opts.name,
|
|
6
|
-
description: opts.description,
|
|
7
|
-
url: opts.url,
|
|
8
|
-
version: opts.version ?? "1.0.0",
|
|
9
|
-
capabilities: {
|
|
10
|
-
streaming: opts.streaming ?? false,
|
|
11
|
-
pushNotifications: false,
|
|
12
|
-
stateTransitionHistory: true,
|
|
13
|
-
},
|
|
14
|
-
skills: opts.skills ?? [],
|
|
15
|
-
defaultInputModes: ["text"],
|
|
16
|
-
defaultOutputModes: ["text"],
|
|
17
|
-
};
|
|
18
|
-
}
|
package/src/client/index.ts
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import type { A2AClientConfig, A2ATask, AgentCard } from "../types.js";
|
|
2
|
-
import { A2AAuthExpiredError } from "../errors.js";
|
|
3
|
-
|
|
4
|
-
export class A2AClient {
|
|
5
|
-
constructor(private readonly config: A2AClientConfig) {
|
|
6
|
-
// Validate baseUrl scheme to prevent SSRF via file://, ftp://, or other
|
|
7
|
-
// non-HTTP schemes when baseUrl is derived from untrusted agent registry data.
|
|
8
|
-
let parsedUrl: URL;
|
|
9
|
-
try {
|
|
10
|
-
parsedUrl = new URL(config.baseUrl);
|
|
11
|
-
} catch {
|
|
12
|
-
throw new Error(`Invalid A2A baseUrl: "${config.baseUrl}" is not a valid URL`);
|
|
13
|
-
}
|
|
14
|
-
if (parsedUrl.protocol !== "https:" && parsedUrl.protocol !== "http:") {
|
|
15
|
-
throw new Error(`Invalid A2A baseUrl scheme: "${parsedUrl.protocol}" — only "https:" and "http:" are allowed`);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Throw A2AAuthExpiredError on 401, otherwise return the response unchanged. */
|
|
20
|
-
private assertNotUnauthorized(res: Response, url: string): void {
|
|
21
|
-
if (res.status === 401) {
|
|
22
|
-
throw new A2AAuthExpiredError(
|
|
23
|
-
`A2A request unauthorized (401): credentials may be expired — ${url}`,
|
|
24
|
-
url,
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async getCard(): Promise<AgentCard> {
|
|
30
|
-
const url = `${this.config.baseUrl}/.well-known/agent.json`;
|
|
31
|
-
const res = await fetch(url, {
|
|
32
|
-
headers: this.config.headers,
|
|
33
|
-
});
|
|
34
|
-
this.assertNotUnauthorized(res, url);
|
|
35
|
-
if (!res.ok) throw new Error(`Failed to fetch agent card: ${res.status}`);
|
|
36
|
-
return res.json() as Promise<AgentCard>;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async sendTask(message: string, opts?: { sessionId?: string }): Promise<string> {
|
|
40
|
-
const taskId = opts?.sessionId ?? crypto.randomUUID();
|
|
41
|
-
const body = {
|
|
42
|
-
jsonrpc: "2.0",
|
|
43
|
-
id: 1,
|
|
44
|
-
method: "tasks/send",
|
|
45
|
-
params: {
|
|
46
|
-
id: taskId,
|
|
47
|
-
message: {
|
|
48
|
-
role: "user",
|
|
49
|
-
parts: [{ type: "text", text: message }],
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const res = await fetch(this.config.baseUrl, {
|
|
55
|
-
method: "POST",
|
|
56
|
-
headers: {
|
|
57
|
-
"Content-Type": "application/json",
|
|
58
|
-
...this.config.headers,
|
|
59
|
-
},
|
|
60
|
-
body: JSON.stringify(body),
|
|
61
|
-
signal: this.config.timeout
|
|
62
|
-
? AbortSignal.timeout(this.config.timeout)
|
|
63
|
-
: undefined,
|
|
64
|
-
});
|
|
65
|
-
this.assertNotUnauthorized(res, this.config.baseUrl);
|
|
66
|
-
if (!res.ok) throw new Error(`A2A tasks/send failed: ${res.status}`);
|
|
67
|
-
const result = (await res.json()) as { result?: A2ATask; error?: { message: string } };
|
|
68
|
-
if (result.error) throw new Error(`A2A error: ${result.error.message}`);
|
|
69
|
-
if (!result.result) {
|
|
70
|
-
throw new Error(`Server returned invalid JSON-RPC response: missing "result" field`);
|
|
71
|
-
}
|
|
72
|
-
const task = result.result;
|
|
73
|
-
return task.artifacts?.[0]?.parts[0]?.type === "text"
|
|
74
|
-
? task.artifacts[0].parts[0].text
|
|
75
|
-
: task.status.message?.parts?.[0]?.type === "text"
|
|
76
|
-
? task.status.message.parts[0].text
|
|
77
|
-
: JSON.stringify(task);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async *streamTask(message: string): AsyncGenerator<string> {
|
|
81
|
-
const taskId = crypto.randomUUID();
|
|
82
|
-
const body = {
|
|
83
|
-
jsonrpc: "2.0",
|
|
84
|
-
id: 1,
|
|
85
|
-
method: "tasks/sendSubscribe",
|
|
86
|
-
params: {
|
|
87
|
-
id: taskId,
|
|
88
|
-
message: { role: "user", parts: [{ type: "text", text: message }] },
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const res = await fetch(this.config.baseUrl, {
|
|
93
|
-
method: "POST",
|
|
94
|
-
headers: { "Content-Type": "application/json", Accept: "text/event-stream", ...this.config.headers },
|
|
95
|
-
body: JSON.stringify(body),
|
|
96
|
-
signal: this.config.timeout
|
|
97
|
-
? AbortSignal.timeout(this.config.timeout)
|
|
98
|
-
: undefined,
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
this.assertNotUnauthorized(res, this.config.baseUrl);
|
|
102
|
-
if (!res.ok || !res.body) throw new Error(`A2A stream failed: ${res.status}`);
|
|
103
|
-
|
|
104
|
-
const reader: ReadableStreamDefaultReader<Uint8Array> = res.body.getReader();
|
|
105
|
-
const decoder = new TextDecoder();
|
|
106
|
-
let buffer = "";
|
|
107
|
-
const readTimeoutMs = this.config.timeout ?? 30_000;
|
|
108
|
-
|
|
109
|
-
const readWithTimeout = async (
|
|
110
|
-
r: ReadableStreamDefaultReader<Uint8Array>,
|
|
111
|
-
timeoutMs: number,
|
|
112
|
-
) => {
|
|
113
|
-
const timer = setTimeout(() => {
|
|
114
|
-
void r.cancel("A2A stream read timeout");
|
|
115
|
-
}, timeoutMs);
|
|
116
|
-
try {
|
|
117
|
-
return await r.read();
|
|
118
|
-
} finally {
|
|
119
|
-
clearTimeout(timer);
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
while (true) {
|
|
125
|
-
const { done, value } = await readWithTimeout(reader, readTimeoutMs);
|
|
126
|
-
if (done) break;
|
|
127
|
-
buffer += decoder.decode(value, { stream: true });
|
|
128
|
-
const lines = buffer.split("\n");
|
|
129
|
-
buffer = lines.pop() ?? "";
|
|
130
|
-
for (const line of lines) {
|
|
131
|
-
if (line.startsWith("data: ")) {
|
|
132
|
-
const data = line.slice(6).trim();
|
|
133
|
-
if (data === "[DONE]") return;
|
|
134
|
-
try {
|
|
135
|
-
const event = JSON.parse(data) as { result?: A2ATask };
|
|
136
|
-
const text = event.result?.status?.message?.parts?.[0];
|
|
137
|
-
// BUG-0028: A2A TextPart declares `text` as optional, meaning
|
|
138
|
-
// text.text may be undefined even when type === "text". The != null
|
|
139
|
-
// check ensures we never yield undefined to stream consumers.
|
|
140
|
-
if (text?.type === "text" && text.text != null) yield text.text;
|
|
141
|
-
} catch { /* ignore parse errors */ }
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
} finally {
|
|
146
|
-
reader.releaseLock();
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Convert this A2A client into an AP3X ToolDefinition-compatible object */
|
|
151
|
-
asToolDefinition(opts: { name: string; description: string }): {
|
|
152
|
-
name: string;
|
|
153
|
-
description: string;
|
|
154
|
-
schema: { type: "object"; properties: { message: { type: "string"; description: string } }; required: string[] };
|
|
155
|
-
parallelSafe: boolean;
|
|
156
|
-
execute: (input: { message: string }) => Promise<string>;
|
|
157
|
-
} {
|
|
158
|
-
return {
|
|
159
|
-
name: opts.name,
|
|
160
|
-
description: opts.description,
|
|
161
|
-
schema: {
|
|
162
|
-
type: "object",
|
|
163
|
-
properties: {
|
|
164
|
-
message: { type: "string", description: "Message to send to the remote agent" },
|
|
165
|
-
},
|
|
166
|
-
required: ["message"],
|
|
167
|
-
},
|
|
168
|
-
parallelSafe: true,
|
|
169
|
-
execute: (input: { message: string }) => this.sendTask(input.message),
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
}
|
package/src/errors.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thrown when an A2A request receives a 401 Unauthorized response,
|
|
3
|
-
* indicating that auth credentials (e.g. an OAuth2 token) have expired
|
|
4
|
-
* or are invalid.
|
|
5
|
-
*
|
|
6
|
-
* Callers can catch this specific error type to trigger token refresh
|
|
7
|
-
* and retry logic without needing changes to A2AClientConfig.
|
|
8
|
-
*/
|
|
9
|
-
export class A2AAuthExpiredError extends Error {
|
|
10
|
-
/** HTTP status code (always 401). */
|
|
11
|
-
readonly statusCode = 401;
|
|
12
|
-
|
|
13
|
-
constructor(
|
|
14
|
-
message: string,
|
|
15
|
-
/** The URL that returned 401. */
|
|
16
|
-
public readonly url: string,
|
|
17
|
-
) {
|
|
18
|
-
super(message);
|
|
19
|
-
this.name = "A2AAuthExpiredError";
|
|
20
|
-
}
|
|
21
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export { A2AAuthExpiredError } from "./errors.js";
|
|
2
|
-
export { generateAgentCard } from "./card/generator.js";
|
|
3
|
-
export { A2AClient } from "./client/index.js";
|
|
4
|
-
export { A2AServer } from "./server/index.js";
|
|
5
|
-
export { a2aAgent } from "./swarm/a2a-agent.js";
|
|
6
|
-
export type {
|
|
7
|
-
AgentCard,
|
|
8
|
-
A2AMessage,
|
|
9
|
-
A2ATask,
|
|
10
|
-
A2AClientConfig,
|
|
11
|
-
AgentSkill,
|
|
12
|
-
AgentCapabilities,
|
|
13
|
-
GenerateCardOptions,
|
|
14
|
-
} from "./types.js";
|
|
15
|
-
export type { A2AAgentDef } from "./swarm/a2a-agent.js";
|
package/src/server/handler.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import type { A2ATask } from "../types.js";
|
|
2
|
-
|
|
3
|
-
export type TaskHandler = (
|
|
4
|
-
message: string,
|
|
5
|
-
taskId: string,
|
|
6
|
-
) => string | Promise<string> | AsyncGenerator<string>;
|
|
7
|
-
|
|
8
|
-
/** Extract a safe error message from an unknown throw value (BUG-0029). */
|
|
9
|
-
function errorMessage(e: unknown): string {
|
|
10
|
-
return e instanceof Error ? e.message : String(e);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function isAsyncGenerator(value: unknown): value is AsyncGenerator<string> {
|
|
14
|
-
return (
|
|
15
|
-
typeof value === "object" &&
|
|
16
|
-
value !== null &&
|
|
17
|
-
typeof (value as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator] === "function"
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export async function handleJsonRPC(
|
|
22
|
-
body: unknown,
|
|
23
|
-
handler: TaskHandler,
|
|
24
|
-
agentCard: import("../types.js").AgentCard,
|
|
25
|
-
): Promise<{ response?: unknown; stream?: AsyncGenerator<string>; taskId?: string }> {
|
|
26
|
-
const req = body as { jsonrpc: string; id: unknown; method: string; params?: { id?: string; message?: { parts?: Array<{ type: string; text?: string }> } } };
|
|
27
|
-
|
|
28
|
-
if (req.jsonrpc !== "2.0") {
|
|
29
|
-
return { response: { jsonrpc: "2.0", id: req.id, error: { code: -32600, message: "Invalid Request" } } };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const taskId = req.params?.id ?? crypto.randomUUID();
|
|
33
|
-
const messageText = req.params?.message?.parts?.find(p => p.type === "text")?.text ?? "";
|
|
34
|
-
|
|
35
|
-
if (req.method === "tasks/send") {
|
|
36
|
-
try {
|
|
37
|
-
const result = await handler(messageText, taskId);
|
|
38
|
-
let output: string;
|
|
39
|
-
if (isAsyncGenerator(result)) {
|
|
40
|
-
const chunks: string[] = [];
|
|
41
|
-
for await (const chunk of result) chunks.push(chunk);
|
|
42
|
-
output = chunks.join("");
|
|
43
|
-
} else {
|
|
44
|
-
output = result;
|
|
45
|
-
}
|
|
46
|
-
const task: A2ATask = {
|
|
47
|
-
id: taskId,
|
|
48
|
-
status: { state: "completed" },
|
|
49
|
-
artifacts: [{ parts: [{ type: "text", text: output }] }],
|
|
50
|
-
};
|
|
51
|
-
return { response: { jsonrpc: "2.0", id: req.id, result: task } };
|
|
52
|
-
} catch (e: unknown) {
|
|
53
|
-
console.error("[a2a] tasks/send error:", errorMessage(e));
|
|
54
|
-
return { response: { jsonrpc: "2.0", id: req.id, error: { code: -32603, message: "Internal server error" } } };
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (req.method === "tasks/sendSubscribe") {
|
|
59
|
-
// If agentCard is provided and streaming is not enabled, reject with method-not-found
|
|
60
|
-
if (agentCard && !agentCard.capabilities?.streaming) {
|
|
61
|
-
return { response: { jsonrpc: "2.0", id: req.id, error: { code: -32601, message: "Method not found: streaming not supported" } } };
|
|
62
|
-
}
|
|
63
|
-
try {
|
|
64
|
-
const result = await handler(messageText, taskId);
|
|
65
|
-
async function* safeStream(gen: AsyncGenerator<string>): AsyncGenerator<string> {
|
|
66
|
-
yield* gen;
|
|
67
|
-
}
|
|
68
|
-
if (isAsyncGenerator(result)) {
|
|
69
|
-
return { stream: safeStream(result), taskId };
|
|
70
|
-
}
|
|
71
|
-
const output: string = result;
|
|
72
|
-
// Wrap promise as single-item stream
|
|
73
|
-
async function* single(): AsyncGenerator<string> {
|
|
74
|
-
yield output;
|
|
75
|
-
}
|
|
76
|
-
return { stream: safeStream(single()), taskId };
|
|
77
|
-
} catch (e: unknown) {
|
|
78
|
-
console.error("[a2a] tasks/sendSubscribe error:", errorMessage(e));
|
|
79
|
-
return { response: { jsonrpc: "2.0", id: req.id, error: { code: -32603, message: errorMessage(e) } } };
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (req.method === "tasks/get") {
|
|
84
|
-
// Basic: return empty completed task (no persistence in this server)
|
|
85
|
-
const task: A2ATask = { id: taskId, status: { state: "completed" } };
|
|
86
|
-
return { response: { jsonrpc: "2.0", id: req.id, result: task } };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return { response: { jsonrpc: "2.0", id: req.id, error: { code: -32601, message: "Method not found" } } };
|
|
90
|
-
}
|
package/src/server/index.ts
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
import type { AgentCard } from "../types.js";
|
|
3
|
-
import { handleJsonRPC, type TaskHandler } from "./handler.js";
|
|
4
|
-
import { createSSEResponse } from "./sse.js";
|
|
5
|
-
|
|
6
|
-
const MAX_BODY_SIZE = 1 * 1024 * 1024; // 1 MB
|
|
7
|
-
|
|
8
|
-
const SECURITY_HEADERS: Record<string, string> = {
|
|
9
|
-
"X-Content-Type-Options": "nosniff",
|
|
10
|
-
"X-Frame-Options": "DENY",
|
|
11
|
-
"Content-Security-Policy": "default-src 'none'",
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export interface A2AServerOptions {
|
|
15
|
-
/** API key for Bearer token authentication. When set, every request (except
|
|
16
|
-
* CORS preflight) must include an `Authorization: Bearer <key>` header.
|
|
17
|
-
*
|
|
18
|
-
* BUG-0256: In production (NODE_ENV=production), apiKey is required unless
|
|
19
|
-
* `allowUnauthenticated` is explicitly set to true. */
|
|
20
|
-
apiKey?: string;
|
|
21
|
-
/** Explicitly opt out of authentication. Required in production when no
|
|
22
|
-
* apiKey is provided. Defaults to false. */
|
|
23
|
-
allowUnauthenticated?: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export class A2AServer {
|
|
27
|
-
private readonly apiKey?: string;
|
|
28
|
-
|
|
29
|
-
constructor(
|
|
30
|
-
private readonly agentCard: AgentCard,
|
|
31
|
-
private readonly handler: TaskHandler,
|
|
32
|
-
options?: A2AServerOptions,
|
|
33
|
-
) {
|
|
34
|
-
this.apiKey = options?.apiKey;
|
|
35
|
-
if (!this.apiKey) {
|
|
36
|
-
if (process.env.NODE_ENV === "production" && !options?.allowUnauthenticated) {
|
|
37
|
-
throw new Error(
|
|
38
|
-
"[A2AServer] apiKey is required in production. " +
|
|
39
|
-
"Set options.apiKey or pass allowUnauthenticated: true to explicitly opt out.",
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
console.warn("[A2AServer] WARNING: No apiKey configured — all endpoints are unauthenticated");
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
requestHandler(): (req: Request) => Promise<Response> {
|
|
47
|
-
return async (req: Request): Promise<Response> => {
|
|
48
|
-
const url = new URL(req.url);
|
|
49
|
-
|
|
50
|
-
// Handle CORS preflight — deny all cross-origin requests by returning
|
|
51
|
-
// no Access-Control-Allow-Origin header, which causes the browser to
|
|
52
|
-
// block the subsequent actual request.
|
|
53
|
-
if (req.method === "OPTIONS") {
|
|
54
|
-
return new Response(null, { status: 204, headers: { ...SECURITY_HEADERS } });
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Optional API key authentication — if configured, require a matching
|
|
58
|
-
// Bearer token on every non-preflight request.
|
|
59
|
-
if (this.apiKey) {
|
|
60
|
-
const token = req.headers.get("authorization") ?? "";
|
|
61
|
-
const expected = `Bearer ${this.apiKey}`;
|
|
62
|
-
const tokenBuf = Buffer.from(token);
|
|
63
|
-
const expectedBuf = Buffer.from(expected);
|
|
64
|
-
if (tokenBuf.length !== expectedBuf.length || !crypto.timingSafeEqual(tokenBuf, expectedBuf)) {
|
|
65
|
-
return new Response(
|
|
66
|
-
JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32600, message: "Unauthorized" } }),
|
|
67
|
-
{ status: 401, headers: { "Content-Type": "application/json", ...SECURITY_HEADERS } },
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (req.method === "GET" && url.pathname === "/.well-known/agent.json") {
|
|
73
|
-
return new Response(JSON.stringify(this.agentCard), {
|
|
74
|
-
headers: { "Content-Type": "application/json", ...SECURITY_HEADERS },
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (req.method !== "POST") {
|
|
79
|
-
return new Response("Method Not Allowed", { status: 405, headers: { ...SECURITY_HEADERS } });
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Require application/json Content-Type on POST requests. This is not a
|
|
83
|
-
// CORS "simple" content type, so browsers must send a preflight OPTIONS
|
|
84
|
-
// request first. Since we return no CORS allow headers, the preflight
|
|
85
|
-
// fails and the cross-origin POST is never sent — preventing CSRF.
|
|
86
|
-
const ct = req.headers.get("content-type") ?? "";
|
|
87
|
-
if (!ct.includes("application/json")) {
|
|
88
|
-
return new Response(
|
|
89
|
-
JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32600, message: "Content-Type must be application/json" } }),
|
|
90
|
-
{ status: 415, headers: { "Content-Type": "application/json", ...SECURITY_HEADERS } },
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Enforce body size limit for Fetch-API callers (mirrors listen() guard)
|
|
95
|
-
const contentLength = parseInt(req.headers.get("content-length") ?? "", 10);
|
|
96
|
-
if (contentLength > MAX_BODY_SIZE) {
|
|
97
|
-
return new Response(JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32600, message: "Request body too large" } }), { status: 413, headers: { "Content-Type": "application/json", ...SECURITY_HEADERS } });
|
|
98
|
-
}
|
|
99
|
-
let body: unknown;
|
|
100
|
-
try {
|
|
101
|
-
const text = await req.text();
|
|
102
|
-
if (text.length > MAX_BODY_SIZE) {
|
|
103
|
-
return new Response(JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32600, message: "Request body too large" } }), { status: 413, headers: { "Content-Type": "application/json", ...SECURITY_HEADERS } });
|
|
104
|
-
}
|
|
105
|
-
body = JSON.parse(text);
|
|
106
|
-
}
|
|
107
|
-
catch { return new Response(JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } }), { status: 400, headers: { "Content-Type": "application/json", ...SECURITY_HEADERS } }); }
|
|
108
|
-
|
|
109
|
-
const acceptsSSE = req.headers.get("Accept")?.includes("text/event-stream");
|
|
110
|
-
const result = await handleJsonRPC(body, this.handler, this.agentCard);
|
|
111
|
-
|
|
112
|
-
if (result.stream && acceptsSSE) {
|
|
113
|
-
return createSSEResponse(result.stream, result.taskId ?? "");
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (result.stream) {
|
|
117
|
-
// Client does not accept SSE but the handler returned a stream.
|
|
118
|
-
// Close the AsyncGenerator to prevent a resource leak, then return
|
|
119
|
-
// a 406 so the client knows it must use Accept: text/event-stream.
|
|
120
|
-
await result.stream.return(undefined);
|
|
121
|
-
return new Response(
|
|
122
|
-
JSON.stringify({
|
|
123
|
-
jsonrpc: "2.0",
|
|
124
|
-
id: (body as Record<string, unknown>)?.id ?? null,
|
|
125
|
-
error: { code: -32600, message: "This method requires Accept: text/event-stream" },
|
|
126
|
-
}),
|
|
127
|
-
{ status: 406, headers: { "Content-Type": "application/json", ...SECURITY_HEADERS } },
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return new Response(JSON.stringify(result.response), {
|
|
132
|
-
headers: { "Content-Type": "application/json", ...SECURITY_HEADERS },
|
|
133
|
-
});
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async listen(port = 3000): Promise<void> {
|
|
138
|
-
const { createServer } = await import("node:http");
|
|
139
|
-
const handler = this.requestHandler();
|
|
140
|
-
const server = createServer(async (req, res) => {
|
|
141
|
-
const url = `http://localhost:${port}${req.url}`;
|
|
142
|
-
const headers: Record<string, string> = {};
|
|
143
|
-
for (const [k, v] of Object.entries(req.headers)) {
|
|
144
|
-
if (typeof v === "string") headers[k] = v;
|
|
145
|
-
}
|
|
146
|
-
let body: Buffer | undefined;
|
|
147
|
-
try {
|
|
148
|
-
body = req.method !== "GET" && req.method !== "HEAD"
|
|
149
|
-
? await new Promise<Buffer>((resolve, reject) => {
|
|
150
|
-
const chunks: Buffer[] = [];
|
|
151
|
-
let totalBytes = 0;
|
|
152
|
-
req.on("data", (c: Buffer) => {
|
|
153
|
-
totalBytes += c.length;
|
|
154
|
-
if (totalBytes > MAX_BODY_SIZE) {
|
|
155
|
-
req.destroy();
|
|
156
|
-
res.writeHead(413, { "Content-Type": "text/plain", ...SECURITY_HEADERS });
|
|
157
|
-
res.end("Payload Too Large");
|
|
158
|
-
reject(new Error("Payload Too Large"));
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
chunks.push(c);
|
|
162
|
-
});
|
|
163
|
-
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
164
|
-
req.on("error", reject);
|
|
165
|
-
})
|
|
166
|
-
: undefined;
|
|
167
|
-
} catch {
|
|
168
|
-
return; // already responded with 413
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
try {
|
|
172
|
-
const fetchReq = new Request(url, { method: req.method, headers, body });
|
|
173
|
-
const fetchRes = await handler(fetchReq);
|
|
174
|
-
|
|
175
|
-
res.writeHead(fetchRes.status, { ...Object.fromEntries(fetchRes.headers.entries()), ...SECURITY_HEADERS });
|
|
176
|
-
const contentType = fetchRes.headers.get("content-type") ?? "";
|
|
177
|
-
if (contentType.includes("text/event-stream") && fetchRes.body) {
|
|
178
|
-
const reader = fetchRes.body.getReader();
|
|
179
|
-
try {
|
|
180
|
-
while (true) {
|
|
181
|
-
const { done, value } = await reader.read();
|
|
182
|
-
if (done) break;
|
|
183
|
-
res.write(value);
|
|
184
|
-
}
|
|
185
|
-
} finally {
|
|
186
|
-
res.end();
|
|
187
|
-
}
|
|
188
|
-
} else {
|
|
189
|
-
const buf = await fetchRes.arrayBuffer();
|
|
190
|
-
res.end(Buffer.from(buf));
|
|
191
|
-
}
|
|
192
|
-
} catch {
|
|
193
|
-
if (!res.headersSent) {
|
|
194
|
-
res.writeHead(500, { "Content-Type": "text/plain", ...SECURITY_HEADERS });
|
|
195
|
-
}
|
|
196
|
-
if (!res.writableEnded) {
|
|
197
|
-
res.end("Internal Server Error");
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
await new Promise<void>((resolve, reject) => {
|
|
203
|
-
server.listen(port, () => resolve());
|
|
204
|
-
server.once("error", reject);
|
|
205
|
-
});
|
|
206
|
-
console.log(`A2AServer listening on port ${port}`);
|
|
207
|
-
}
|
|
208
|
-
}
|
package/src/server/sse.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
export function createSSEResponse(
|
|
2
|
-
stream: AsyncGenerator<string>,
|
|
3
|
-
taskId: string,
|
|
4
|
-
): Response {
|
|
5
|
-
const encoder = new TextEncoder();
|
|
6
|
-
const readable = new ReadableStream({
|
|
7
|
-
async start(controller) {
|
|
8
|
-
try {
|
|
9
|
-
for await (const chunk of stream) {
|
|
10
|
-
const event = JSON.stringify({
|
|
11
|
-
result: {
|
|
12
|
-
id: taskId,
|
|
13
|
-
status: { state: "working", message: { role: "agent", parts: [{ type: "text", text: chunk }] } },
|
|
14
|
-
},
|
|
15
|
-
});
|
|
16
|
-
controller.enqueue(encoder.encode(`data: ${event}\n\n`));
|
|
17
|
-
}
|
|
18
|
-
const doneEvent = JSON.stringify({
|
|
19
|
-
result: { id: taskId, status: { state: "completed" } },
|
|
20
|
-
});
|
|
21
|
-
controller.enqueue(encoder.encode(`data: ${doneEvent}\n\n`));
|
|
22
|
-
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
23
|
-
} catch (err) {
|
|
24
|
-
console.error("[a2a] SSE task handler error:", err);
|
|
25
|
-
const errEvent = JSON.stringify({ error: { code: -32603, message: "Internal server error" } });
|
|
26
|
-
try {
|
|
27
|
-
controller.enqueue(encoder.encode(`data: ${errEvent}\n\n`));
|
|
28
|
-
} catch { /* stream may already be cancelled */ }
|
|
29
|
-
controller.error(err);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
controller.close();
|
|
33
|
-
},
|
|
34
|
-
cancel() {
|
|
35
|
-
stream.return(undefined);
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
return new Response(readable, {
|
|
39
|
-
headers: {
|
|
40
|
-
"Content-Type": "text/event-stream",
|
|
41
|
-
"Cache-Control": "no-cache",
|
|
42
|
-
Connection: "keep-alive",
|
|
43
|
-
"X-Content-Type-Options": "nosniff",
|
|
44
|
-
"X-Frame-Options": "DENY",
|
|
45
|
-
"Content-Security-Policy": "default-src 'none'",
|
|
46
|
-
"X-XSS-Protection": "0",
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
}
|
package/src/swarm/a2a-agent.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { A2AClient } from "../client/index.js";
|
|
2
|
-
import type { A2AClientConfig } from "../types.js";
|
|
3
|
-
|
|
4
|
-
export interface A2AAgentDef {
|
|
5
|
-
id: string;
|
|
6
|
-
role: string;
|
|
7
|
-
/** Call this to invoke the remote A2A agent */
|
|
8
|
-
invoke: (message: string) => Promise<string>;
|
|
9
|
-
/** Compatible with AP3X ToolDefinition for use in agent loops */
|
|
10
|
-
asTool: () => {
|
|
11
|
-
name: string;
|
|
12
|
-
description: string;
|
|
13
|
-
schema: { type: "object"; properties: { message: { type: "string" } }; required: string[] };
|
|
14
|
-
parallelSafe: boolean;
|
|
15
|
-
execute: (input: { message: string }) => Promise<string>;
|
|
16
|
-
};
|
|
17
|
-
capabilities?: string[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function a2aAgent(opts: {
|
|
21
|
-
id: string;
|
|
22
|
-
role: string;
|
|
23
|
-
clientConfig: A2AClientConfig;
|
|
24
|
-
capabilities?: string[];
|
|
25
|
-
}): A2AAgentDef {
|
|
26
|
-
const client = new A2AClient(opts.clientConfig);
|
|
27
|
-
return {
|
|
28
|
-
id: opts.id,
|
|
29
|
-
role: opts.role,
|
|
30
|
-
capabilities: opts.capabilities,
|
|
31
|
-
invoke: (message) => client.sendTask(message),
|
|
32
|
-
asTool: () => client.asToolDefinition({ name: opts.id, description: opts.role }),
|
|
33
|
-
};
|
|
34
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
// A2A Protocol types (based on A2A spec)
|
|
2
|
-
export interface AgentSkill {
|
|
3
|
-
id: string;
|
|
4
|
-
name: string;
|
|
5
|
-
description?: string;
|
|
6
|
-
tags?: string[];
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface AgentCapabilities {
|
|
10
|
-
streaming?: boolean;
|
|
11
|
-
pushNotifications?: boolean;
|
|
12
|
-
stateTransitionHistory?: boolean;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface AgentCard {
|
|
16
|
-
name: string;
|
|
17
|
-
description: string;
|
|
18
|
-
url: string;
|
|
19
|
-
version?: string;
|
|
20
|
-
capabilities: AgentCapabilities;
|
|
21
|
-
skills: AgentSkill[];
|
|
22
|
-
defaultInputModes?: string[];
|
|
23
|
-
defaultOutputModes?: string[];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface A2AMessage {
|
|
27
|
-
role: "user" | "agent";
|
|
28
|
-
parts: Array<{ type: "text"; text: string } | { type: "data"; data: unknown }>;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface A2ATask {
|
|
32
|
-
id: string;
|
|
33
|
-
status: { state: "submitted" | "working" | "completed" | "failed"; message?: A2AMessage };
|
|
34
|
-
artifacts?: Array<{ name?: string; parts: A2AMessage["parts"] }>;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface A2AClientConfig {
|
|
38
|
-
baseUrl: string;
|
|
39
|
-
headers?: Record<string, string>;
|
|
40
|
-
timeout?: number;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface GenerateCardOptions {
|
|
44
|
-
name: string;
|
|
45
|
-
description: string;
|
|
46
|
-
url: string;
|
|
47
|
-
version?: string;
|
|
48
|
-
skills?: AgentSkill[];
|
|
49
|
-
streaming?: boolean;
|
|
50
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"lib": ["ES2022"],
|
|
7
|
-
"rootDir": "./src",
|
|
8
|
-
"outDir": "./dist",
|
|
9
|
-
"declaration": true,
|
|
10
|
-
"declarationDir": "./dist",
|
|
11
|
-
"sourceMap": true,
|
|
12
|
-
"strict": true,
|
|
13
|
-
"noUncheckedIndexedAccess": false,
|
|
14
|
-
"noImplicitOverride": true,
|
|
15
|
-
"exactOptionalPropertyTypes": false,
|
|
16
|
-
"skipLibCheck": true,
|
|
17
|
-
"forceConsistentCasingInFileNames": true
|
|
18
|
-
},
|
|
19
|
-
"include": ["src/**/*"]
|
|
20
|
-
}
|