a2a-exposed 0.8.15 → 0.8.16
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/lib/deploy.mjs +33 -6
- package/lib/main.mjs +2 -1
- package/package.json +1 -1
- package/worker/src/index.ts +35 -14
- package/worker/src/version.ts +1 -1
- package/worker/test/facade.test.ts +56 -1
- package/worker/test/version-header.test.ts +100 -0
package/lib/deploy.mjs
CHANGED
|
@@ -434,15 +434,41 @@ async function deployAndLearn(dir, secrets, o) {
|
|
|
434
434
|
* whoami before npm install. A previous install has node_modules/.bin/cf.
|
|
435
435
|
* A fresh init does not: `npx cf auth login` does not put `cf` on PATH, so this check uses `npx --yes cf`.
|
|
436
436
|
*/
|
|
437
|
-
function
|
|
437
|
+
function cfSpawn(dir, args, { allowFail = false } = {}) {
|
|
438
438
|
const local = path.join(dir, "node_modules", ".bin", "cf");
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
? run(local,
|
|
442
|
-
: run("npx", ["--yes", "cf", ...
|
|
439
|
+
const full = cfArgs(args);
|
|
440
|
+
return fs.existsSync(local)
|
|
441
|
+
? run(local, full, { cwd: dir, capture: true, allowFail })
|
|
442
|
+
: run("npx", ["--yes", "cf", ...full], { cwd: dir, capture: true, allowFail });
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function cfLoginJson(dir) {
|
|
446
|
+
const r = cfSpawn(dir, ["auth", "whoami"]);
|
|
443
447
|
try { return JSON.parse(r.stdout); } catch { die("unexpected output from cf auth whoami"); }
|
|
444
448
|
}
|
|
445
449
|
|
|
450
|
+
function normalizeAccounts(raw) {
|
|
451
|
+
const list = Array.isArray(raw) ? raw : (raw && typeof raw === "object" && Array.isArray(raw.result) ? raw.result : []);
|
|
452
|
+
return list.filter((a) => a && typeof a.id === "string" && a.id).map((a) => ({ id: a.id, name: typeof a.name === "string" && a.name ? a.name : a.id }));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** `cf accounts list` (GET /accounts). whoami intersects memberships and can return [] for a logged-in profile. */
|
|
456
|
+
function accountsFromList(dir) {
|
|
457
|
+
const all = [];
|
|
458
|
+
const seen = new Set();
|
|
459
|
+
for (let page = 1; page <= 20; page++) {
|
|
460
|
+
const r = cfSpawn(dir, ["accounts", "list", "--page", String(page), "--per-page", "50"], { allowFail: true });
|
|
461
|
+
if (!r || r.status !== 0) return page === 1 ? [] : all;
|
|
462
|
+
let data;
|
|
463
|
+
try { data = JSON.parse(r.stdout); } catch { return page === 1 ? [] : all; }
|
|
464
|
+
if (data && typeof data === "object" && !Array.isArray(data) && Array.isArray(data.result)) data = data.result;
|
|
465
|
+
if (!Array.isArray(data)) return page === 1 ? [] : all;
|
|
466
|
+
for (const a of normalizeAccounts(data)) if (!seen.has(a.id)) { seen.add(a.id); all.push(a); }
|
|
467
|
+
if (data.length < 50) break;
|
|
468
|
+
}
|
|
469
|
+
return all;
|
|
470
|
+
}
|
|
471
|
+
|
|
446
472
|
/** Cloudflare login, before init installs packages. Dies with the same advice as a later cf failure. */
|
|
447
473
|
function requireCfLogin(dir) {
|
|
448
474
|
const prof = cfProfile();
|
|
@@ -454,7 +480,8 @@ function requireCfLogin(dir) {
|
|
|
454
480
|
: prof
|
|
455
481
|
? `cf profile "${prof}" is not logged in: run \`npx cf auth create ${prof} --no-browser\`, open the printed URL, enter the code, then re-run init`
|
|
456
482
|
: "not logged in to Cloudflare: run `npx cf auth login --no-browser`, open the printed URL, enter the code, then re-run init") + `\n${LOGIN_403_HELP}`);
|
|
457
|
-
|
|
483
|
+
let accts = normalizeAccounts(who.accounts);
|
|
484
|
+
if (!accts.length && prof) accts = accountsFromList(dir);
|
|
458
485
|
if (!C.get("CLOUDFLARE_ACCOUNT_ID")) {
|
|
459
486
|
if (!accts.length && process.env.CLOUDFLARE_API_TOKEN)
|
|
460
487
|
die("the API token can't list accounts: add the permission User -> Memberships -> Read (or pass --account-id <id> / export CLOUDFLARE_ACCOUNT_ID; the id is in the dashboard URL)");
|
package/lib/main.mjs
CHANGED
|
@@ -41,7 +41,8 @@ https://github.com/telegraphic-dev/a2a-exposed/blob/main/docs/getting-started.md
|
|
|
41
41
|
--hostname H moves it back); the old URL answers 301/410
|
|
42
42
|
--workers-dev-subdomain create the account's workers.dev subdomain if missing
|
|
43
43
|
--cf-profile cf auth profile for a separate Cloudflare login
|
|
44
|
-
(npx cf auth create NAME --no-browser); saved, used by every cf call
|
|
44
|
+
(npx cf auth create NAME --no-browser); saved, used by every cf call.
|
|
45
|
+
When whoami lists no accounts, init reads cf accounts list
|
|
45
46
|
--d1-name D1 database to create or reuse (default: worker name)
|
|
46
47
|
--pairing-approval who approves device-flow pairing requests (default human:
|
|
47
48
|
the owner token via \`pair approve\`, or the approval password
|
package/package.json
CHANGED
package/worker/src/index.ts
CHANGED
|
@@ -569,6 +569,31 @@ const V03: Record<string, string> = {
|
|
|
569
569
|
"message/send": "send", "tasks/get": "get", "tasks/cancel": "cancel", "tasks/pushNotificationConfig/set": "pushset",
|
|
570
570
|
"tasks/pushNotificationConfig/get": "pushget", "message/stream": "stream", "tasks/resubscribe": "stream",
|
|
571
571
|
};
|
|
572
|
+
// Methods only the façade forwards (the inbox has one push config per task and no delete).
|
|
573
|
+
const PROXY_V1: Record<string, string> = { ListTaskPushNotificationConfig: "pushlist", DeleteTaskPushNotificationConfig: "pushdel" };
|
|
574
|
+
const PROXY_V03: Record<string, string> = { "tasks/pushNotificationConfig/list": "pushlist", "tasks/pushNotificationConfig/delete": "pushdel" };
|
|
575
|
+
const EXTENDED_CARD_METHODS = new Set(["GetExtendedAgentCard", "agent/getAuthenticatedExtendedCard"]);
|
|
576
|
+
|
|
577
|
+
/** Header, else query `A2A-Version`. A patch (`1.0.1`, `0.3.0`) is ignored. Empty means the method name chooses the
|
|
578
|
+
* shape, so a 0.3 client that omits the header stays on 0.3 and a headerless `SendMessage` stays 1.0. */
|
|
579
|
+
function declaredA2AVersion(req: Request): { raw: string; version: string | null } {
|
|
580
|
+
let raw = (req.headers.get("a2a-version") || "").trim();
|
|
581
|
+
if (!raw) {
|
|
582
|
+
try { raw = (new URL(req.url).searchParams.get("A2A-Version") || "").trim(); } catch { raw = ""; }
|
|
583
|
+
}
|
|
584
|
+
if (!raw) return { raw: "", version: null };
|
|
585
|
+
if (/^(1\.0(\.\d+)?|1)$/.test(raw)) return { raw, version: "1.0" };
|
|
586
|
+
if (/^0\.3(\.\d+)?$/.test(raw)) return { raw, version: "0.3" };
|
|
587
|
+
return { raw, version: null };
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function rpcOperation(method: string, proxy: boolean): { op: string; version: string } | null {
|
|
591
|
+
if (V1[method]) return { op: V1[method], version: "1.0" };
|
|
592
|
+
if (V03[method]) return { op: V03[method], version: "0.3" };
|
|
593
|
+
if (proxy && PROXY_V1[method]) return { op: PROXY_V1[method], version: "1.0" };
|
|
594
|
+
if (proxy && PROXY_V03[method]) return { op: PROXY_V03[method], version: "0.3" };
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
572
597
|
|
|
573
598
|
async function ownedTask(ctx: TenantContext, id: unknown, label: string): Promise<Json> {
|
|
574
599
|
if (typeof id !== "string" || !A.ID_RE.test(id)) throw new RpcError(-32001, "Task not found");
|
|
@@ -713,15 +738,14 @@ async function handleRpc(req: Request, ctx: TenantContext, ectx: ExecutionContex
|
|
|
713
738
|
const err = (code: number, message: string) => json({ jsonrpc: "2.0", id: rid, error: { code, message } });
|
|
714
739
|
if (!rq || typeof rq !== "object" || rq.jsonrpc !== "2.0" || typeof rq.method !== "string") return err(-32600, "Invalid Request");
|
|
715
740
|
const method: string = rq.method;
|
|
716
|
-
let op: string, version: string;
|
|
717
741
|
const proxy = proxyMode(ctx);
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
const
|
|
724
|
-
|
|
742
|
+
const declared = declaredA2AVersion(req);
|
|
743
|
+
if (declared.raw && !declared.version) return err(-32009, `A2A version ${declared.raw} not supported (supported: 0.3, 1.0)`);
|
|
744
|
+
if (EXTENDED_CARD_METHODS.has(method)) return err(-32007, "Extended agent card is not configured");
|
|
745
|
+
const found = rpcOperation(method, proxy);
|
|
746
|
+
if (!found) return err(-32601, "Method not found");
|
|
747
|
+
const version = declared.version || found.version;
|
|
748
|
+
const op = found.op;
|
|
725
749
|
if (op === "stream") return err(-32004, proxy ? "Streaming is not supported through this façade yet" : "Streaming is not supported");
|
|
726
750
|
const params = rq.params ?? {};
|
|
727
751
|
if (typeof params !== "object" || Array.isArray(params)) return err(-32602, "Invalid params");
|
|
@@ -796,10 +820,6 @@ async function verifyUpstream(ctx: TenantContext): Promise<Json> {
|
|
|
796
820
|
return { ok: verdict.reason === "reachable", ...verdict, sent, method: VERIFY_METHOD, upstreamWantsBearer: F.upstreamAuth(up.card).bearer };
|
|
797
821
|
}
|
|
798
822
|
|
|
799
|
-
// Methods only the façade forwards (the inbox has one push config per task and no delete).
|
|
800
|
-
const PROXY_V1: Record<string, string> = { ListTaskPushNotificationConfig: "pushlist", DeleteTaskPushNotificationConfig: "pushdel" };
|
|
801
|
-
const PROXY_V03: Record<string, string> = { "tasks/pushNotificationConfig/list": "pushlist", "tasks/pushNotificationConfig/delete": "pushdel" };
|
|
802
|
-
|
|
803
823
|
async function ownerOf(ctx: TenantContext, kind: "task" | "context", id: string): Promise<string | null> {
|
|
804
824
|
const r: Json = await ctx.DB.prepare("SELECT peer FROM facade_owners WHERE kind = ? AND id = ?").bind(kind, id).first();
|
|
805
825
|
return r ? r.peer : null;
|
|
@@ -847,8 +867,9 @@ async function proxyRpc(req: Request, ctx: TenantContext, c: { raw: string; rid:
|
|
|
847
867
|
// public-looking name to a private address), so push configs never reach it. Follow-up: a relay through the Worker.
|
|
848
868
|
if (F.wantsPush(c.op, c.params)) return err(-32003, "Push notifications are not supported through this façade; poll GetTask with the task id instead");
|
|
849
869
|
const headers = upstreamHeaders(ctx, ep, { "content-type": "application/json", accept: "application/json", "x-a2a-peer": c.label });
|
|
850
|
-
|
|
851
|
-
|
|
870
|
+
// Declared version only (header, else query). A headerless call stays headerless, so the upstream still chooses from the method name.
|
|
871
|
+
const declared = declaredA2AVersion(req);
|
|
872
|
+
if (declared.version) headers["a2a-version"] = declared.version;
|
|
852
873
|
let r: Response;
|
|
853
874
|
try {
|
|
854
875
|
r = await fetch(ep, { method: "POST", headers, body: c.raw, redirect: "manual", signal: AbortSignal.timeout(90000) });
|
package/worker/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Inbox build. Keep this equal to `version` in cli/package.json. */
|
|
2
|
-
export const VERSION = "0.8.
|
|
2
|
+
export const VERSION = "0.8.16";
|
|
@@ -268,6 +268,54 @@ test("proxy mode: JSON-RPC is forwarded with the façade's credential, never the
|
|
|
268
268
|
assert.equal((s.DB.db.prepare("SELECT COUNT(*) AS n FROM tasks").get() as any).n, 0);
|
|
269
269
|
});
|
|
270
270
|
|
|
271
|
+
test("proxy mode: a declared A2A-Version query is forwarded as the header; a headerless call stays headerless", async (t) => {
|
|
272
|
+
let n = 0;
|
|
273
|
+
const s = setup({ UPSTREAM_URL: UPSTREAM, UPSTREAM_CARD_URL: "https://agent-upstream.example.net/card-ver.json" }, async (url, init) => {
|
|
274
|
+
if (url.endsWith(".json")) return Response.json(tailnetCard());
|
|
275
|
+
const rq = JSON.parse(init.body);
|
|
276
|
+
const hv = init.headers?.["a2a-version"];
|
|
277
|
+
const v1 = hv ? hv.startsWith("1") : !String(rq.method).includes("/");
|
|
278
|
+
const id = `up-task-${++n}`, contextId = `up-ctx-${n}`;
|
|
279
|
+
const result = v1
|
|
280
|
+
? { task: { id, contextId, status: { state: "TASK_STATE_SUBMITTED" }, history: [{ role: "ROLE_USER", parts: [{ text: "hi" }] }] } }
|
|
281
|
+
: { kind: "task", id, contextId, status: { state: "submitted" }, history: [{ kind: "message", role: "user", parts: [{ kind: "text", text: "hi" }] }] };
|
|
282
|
+
return Response.json({ jsonrpc: "2.0", id: rq.id, result });
|
|
283
|
+
});
|
|
284
|
+
t.after(s.restore);
|
|
285
|
+
const tok = await s.issue("ada");
|
|
286
|
+
const msg = { message: { role: "user", messageId: "m-ver", parts: [{ kind: "text", text: "hi" }] } };
|
|
287
|
+
const post = (path: string, method: string, headers: Record<string, string> = {}) =>
|
|
288
|
+
s.call("POST", path, { token: tok, json: { jsonrpc: "2.0", id: 1, method, params: msg }, headers });
|
|
289
|
+
const last = () => s.calls.filter((c) => c.url === UPSTREAM).at(-1);
|
|
290
|
+
|
|
291
|
+
const q = await post("/?A2A-Version=0.3", "SendMessage");
|
|
292
|
+
assert.equal(q.status, 200, q.text);
|
|
293
|
+
assert.equal(last().url, UPSTREAM);
|
|
294
|
+
assert.equal(last().headers["a2a-version"], "0.3");
|
|
295
|
+
assert.equal(q.data.result.kind, "task");
|
|
296
|
+
assert.equal(q.data.result.status.state, "submitted");
|
|
297
|
+
assert.equal(q.data.result.task, undefined);
|
|
298
|
+
|
|
299
|
+
const bare = await post("/", "SendMessage");
|
|
300
|
+
assert.equal(last().headers["a2a-version"], undefined);
|
|
301
|
+
assert.equal(bare.data.result.task.status.state, "TASK_STATE_SUBMITTED");
|
|
302
|
+
assert.equal(bare.data.result.kind, undefined);
|
|
303
|
+
|
|
304
|
+
const bare03 = await post("/", "message/send");
|
|
305
|
+
assert.equal(last().headers["a2a-version"], undefined);
|
|
306
|
+
assert.equal(bare03.data.result.kind, "task");
|
|
307
|
+
assert.equal(bare03.data.result.status.state, "submitted");
|
|
308
|
+
|
|
309
|
+
const wins = await post("/?A2A-Version=0.3", "SendMessage", { "a2a-version": "1.0" });
|
|
310
|
+
assert.equal(last().headers["a2a-version"], "1.0");
|
|
311
|
+
assert.equal(wins.data.result.task.status.state, "TASK_STATE_SUBMITTED");
|
|
312
|
+
|
|
313
|
+
const patch = await post("/?A2A-Version=0.3.0", "SendMessage");
|
|
314
|
+
assert.equal(last().headers["a2a-version"], "0.3");
|
|
315
|
+
assert.equal(patch.data.result.kind, "task");
|
|
316
|
+
assert.equal(patch.data.result.status.state, "submitted");
|
|
317
|
+
});
|
|
318
|
+
|
|
271
319
|
test("proxy mode: peers sharing the upstream identity can't touch each other's tasks or contexts", async (t) => {
|
|
272
320
|
const s = setup({ UPSTREAM_URL: UPSTREAM, UPSTREAM_CARD_URL: "https://agent-upstream.example.net/card-c.json" });
|
|
273
321
|
t.after(s.restore);
|
|
@@ -309,7 +357,14 @@ test("proxy mode: peers sharing the upstream identity can't touch each other's t
|
|
|
309
357
|
// ListTasks would list every peer's tasks; streaming is not proxied yet
|
|
310
358
|
assert.equal((await s.rpc(a, "ListTasks", {})).data.error?.code, -32004);
|
|
311
359
|
assert.equal((await s.rpc(a, "SendStreamingMessage", { message: { role: "ROLE_USER", parts: [{ text: "x" }] } })).data.error?.code, -32004);
|
|
312
|
-
|
|
360
|
+
const nCard = s.calls.filter((c) => c.url === UPSTREAM).length;
|
|
361
|
+
const ext = await s.rpc(a, "GetExtendedAgentCard", {});
|
|
362
|
+
assert.equal(ext.status, 200);
|
|
363
|
+
assert.equal(ext.data.error?.code, -32007);
|
|
364
|
+
assert.equal(ext.data.error?.message, "Extended agent card is not configured");
|
|
365
|
+
const ext03 = await s.call("POST", "/", { token: a, json: { jsonrpc: "2.0", id: 1, method: "agent/getAuthenticatedExtendedCard", params: {} }, headers: { "a2a-version": "0.3" } });
|
|
366
|
+
assert.equal(ext03.data.error?.code, -32007);
|
|
367
|
+
assert.equal(s.calls.filter((c) => c.url === UPSTREAM).length, nCard, "the extended card stays on the façade");
|
|
313
368
|
});
|
|
314
369
|
|
|
315
370
|
test("proxy mode: push configs never reach the upstream (it would call them from inside the private network)", async (t) => {
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// A2A-Version selects the response shape; the extended card is -32007. Run: npm test
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import worker from "../src/index.ts";
|
|
5
|
+
import { d1 } from "./d1.ts";
|
|
6
|
+
|
|
7
|
+
const BASE = "https://agent.example.com";
|
|
8
|
+
|
|
9
|
+
function setup() {
|
|
10
|
+
const DB = d1(new URL("../migrations/", import.meta.url));
|
|
11
|
+
const env: any = { DB, PUBLIC_URL: BASE, OWNER_TOKEN: "owner-secret", AGENT_NAME: "Test Inbox", PAIRING_APPROVAL: "off" };
|
|
12
|
+
const call = async (method: string, path: string, o: { json?: any; headers?: Record<string, string> } = {}) => {
|
|
13
|
+
const headers: Record<string, string> = { "cf-connecting-ip": "198.51.100.7", ...(o.headers || {}) };
|
|
14
|
+
let body: string | undefined;
|
|
15
|
+
if (o.json !== undefined) { headers["content-type"] = "application/json"; body = JSON.stringify(o.json); }
|
|
16
|
+
const pending: Promise<unknown>[] = [];
|
|
17
|
+
const res = await worker.fetch(new Request(BASE + path, { method, headers, body }) as any, env, { waitUntil: (p: Promise<unknown>) => pending.push(p), passThroughOnException() {} } as any);
|
|
18
|
+
await Promise.allSettled(pending);
|
|
19
|
+
const text = await res.text();
|
|
20
|
+
let data: any = text;
|
|
21
|
+
try { data = JSON.parse(text); } catch { /* text */ }
|
|
22
|
+
return { status: res.status, data, text };
|
|
23
|
+
};
|
|
24
|
+
return { call };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test("A2A-Version selects the response shape; an absent header keeps the method name", async () => {
|
|
28
|
+
const s = setup();
|
|
29
|
+
const issued = await s.call("POST", "/owner/peers", { json: { label: "ada" }, headers: { authorization: "Bearer owner-secret" } });
|
|
30
|
+
assert.equal(issued.status, 200, issued.text);
|
|
31
|
+
const token = issued.data.token as string;
|
|
32
|
+
let n = 0;
|
|
33
|
+
const msg = () => ({ message: { role: "user", messageId: `m${++n}`, parts: [{ kind: "text", text: "hi" }] } });
|
|
34
|
+
const rpc = (method: string, params: any, headers: Record<string, string> = {}, path = "/") =>
|
|
35
|
+
s.call("POST", path, { json: { jsonrpc: "2.0", id: 1, method, params }, headers: { authorization: `Bearer ${token}`, ...headers } });
|
|
36
|
+
|
|
37
|
+
const v1 = await rpc("message/send", msg(), { "a2a-version": "1.0" });
|
|
38
|
+
assert.equal(v1.status, 200, v1.text);
|
|
39
|
+
assert.equal(v1.data.result.task.status.state, "TASK_STATE_SUBMITTED");
|
|
40
|
+
assert.equal(v1.data.result.task.kind, undefined);
|
|
41
|
+
assert.equal(v1.data.result.task.history[0].role, "ROLE_USER");
|
|
42
|
+
assert.equal(v1.data.result.task.history[0].parts[0].kind, undefined);
|
|
43
|
+
assert.equal(v1.data.result.task.history[0].parts[0].text, "hi");
|
|
44
|
+
|
|
45
|
+
const v03 = await rpc("SendMessage", msg(), { "a2a-version": "0.3" });
|
|
46
|
+
assert.equal(v03.data.result.kind, "task");
|
|
47
|
+
assert.equal(v03.data.result.status.state, "submitted");
|
|
48
|
+
assert.equal(v03.data.result.task, undefined);
|
|
49
|
+
assert.equal(v03.data.result.history[0].role, "user");
|
|
50
|
+
assert.equal(v03.data.result.history[0].kind, "message");
|
|
51
|
+
assert.equal(v03.data.result.history[0].parts[0].kind, "text");
|
|
52
|
+
|
|
53
|
+
const bare03 = await rpc("message/send", msg());
|
|
54
|
+
assert.equal(bare03.data.result.kind, "task");
|
|
55
|
+
assert.equal(bare03.data.result.status.state, "submitted");
|
|
56
|
+
const bare1 = await rpc("SendMessage", msg());
|
|
57
|
+
assert.equal(bare1.data.result.task.status.state, "TASK_STATE_SUBMITTED");
|
|
58
|
+
|
|
59
|
+
const patch1 = await rpc("message/send", msg(), { "a2a-version": "1.0.1" });
|
|
60
|
+
assert.equal(patch1.data.result.task.status.state, "TASK_STATE_SUBMITTED");
|
|
61
|
+
const patch03 = await rpc("SendMessage", msg(), { "A2A-Version": "0.3.0" });
|
|
62
|
+
assert.equal(patch03.data.result.kind, "task");
|
|
63
|
+
const short1 = await rpc("message/send", msg(), { "a2a-version": "1" });
|
|
64
|
+
assert.equal(short1.data.result.task.status.state, "TASK_STATE_SUBMITTED");
|
|
65
|
+
|
|
66
|
+
const query = await rpc("SendMessage", msg(), {}, "/?A2A-Version=0.3");
|
|
67
|
+
assert.equal(query.data.result.kind, "task");
|
|
68
|
+
assert.equal(query.data.result.status.state, "submitted");
|
|
69
|
+
const headerWins = await rpc("SendMessage", msg(), { "a2a-version": "1.0" }, "/?A2A-Version=0.3");
|
|
70
|
+
assert.equal(headerWins.data.result.task.status.state, "TASK_STATE_SUBMITTED");
|
|
71
|
+
|
|
72
|
+
const created = bare1.data.result.task.id as string;
|
|
73
|
+
const reread = await rpc("GetTask", { id: created }, { "a2a-version": "0.3" });
|
|
74
|
+
assert.equal(reread.data.result.kind, "task");
|
|
75
|
+
assert.equal(reread.data.result.status.state, "submitted");
|
|
76
|
+
assert.equal(reread.data.result.id, created);
|
|
77
|
+
|
|
78
|
+
for (const method of ["SendMessage", "no-such-method"]) {
|
|
79
|
+
const bad = await rpc(method, msg(), { "a2a-version": "2.0" });
|
|
80
|
+
assert.equal(bad.data.error?.code, -32009, method);
|
|
81
|
+
assert.match(bad.data.error.message, /2\.0/);
|
|
82
|
+
}
|
|
83
|
+
const minor = await rpc("SendMessage", msg(), { "a2a-version": "1.2" });
|
|
84
|
+
assert.equal(minor.data.error?.code, -32009);
|
|
85
|
+
const unknown = await rpc("no-such-method", {});
|
|
86
|
+
assert.equal(unknown.data.error?.code, -32601);
|
|
87
|
+
const unknownOk = await rpc("no-such-method", {}, { "a2a-version": "1.0" });
|
|
88
|
+
assert.equal(unknownOk.data.error?.code, -32601);
|
|
89
|
+
|
|
90
|
+
for (const method of ["GetExtendedAgentCard", "agent/getAuthenticatedExtendedCard"]) {
|
|
91
|
+
const missing = await rpc(method, {});
|
|
92
|
+
assert.equal(missing.status, 200);
|
|
93
|
+
assert.equal(missing.data.error?.code, -32007, method);
|
|
94
|
+
assert.equal(missing.data.error?.message, "Extended agent card is not configured");
|
|
95
|
+
const withHeader = await rpc(method, {}, { "a2a-version": "0.3" });
|
|
96
|
+
assert.equal(withHeader.data.error?.code, -32007, method);
|
|
97
|
+
}
|
|
98
|
+
const extFirst = await rpc("GetExtendedAgentCard", {}, { "a2a-version": "2.0" });
|
|
99
|
+
assert.equal(extFirst.data.error?.code, -32009);
|
|
100
|
+
});
|