a2a-exposed 0.8.22 → 0.8.24

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.
@@ -929,3 +929,50 @@ test("self-host workerd: human mode owner token approves, the device poll return
929
929
  const deniedToken = await deniedPoll.json() as { error?: string };
930
930
  assert.equal(deniedToken.error, "access_denied");
931
931
  });
932
+
933
+
934
+ test("sealed upstream and wake Access credentials reach only their tenant context in workerd", async (t) => {
935
+ const received: { origin: string; bearer: string | null; id: string | null; secret: string | null }[] = [];
936
+ const mf = await start({
937
+ bindings: { TENANCY: "host", TENANT_DOMAIN: "example.com", TENANT_SECRETS_KEY: "platform-secret" },
938
+ durableObjects: { TENANT_DO: { className: "TenantStore", useSQLite: true } },
939
+ outboundService: async (request: Request) => {
940
+ received.push({ origin: new URL(request.url).origin, bearer: request.headers.get("authorization"), id: request.headers.get("cf-access-client-id"), secret: request.headers.get("cf-access-client-secret") });
941
+ return Response.json({ protocolVersion: "0.3.0", name: "Example", url: "https://upstream.example.com/a2a", capabilities: {}, skills: [] });
942
+ },
943
+ });
944
+ t.after(() => mf.dispose());
945
+ await mf.ready;
946
+ const ns = await mf.getDurableObjectNamespace("TENANT_DO");
947
+ const stub = ns.get(ns.idFromName("sealed-tenant")) as unknown as {
948
+ pushConfig(body: Record<string, unknown>): Promise<{ applied: boolean }>;
949
+ owner(input: { accountId: string; method: string; path: string }): Promise<{ status: number; body: unknown }>;
950
+ };
951
+ const secrets = { UPSTREAM_TOKEN: "dummy-bearer", UPSTREAM_ACCESS_CLIENT_ID: "dummy-upstream-id", UPSTREAM_ACCESS_CLIENT_SECRET: "dummy-upstream-secret", WAKE_ACCESS_CLIENT_ID: "dummy-wake-id", WAKE_ACCESS_CLIENT_SECRET: "dummy-wake-secret" };
952
+ const sealed = await sealWakeSecrets("platform-secret", "sealed-tenant", secrets);
953
+ assert.ok(sealed);
954
+ const push = async (version: number, secretsEnc: string) => stub.pushConfig({
955
+ version, tenantId: "sealed-tenant", name: "sealed", status: "active", ownerTokenHash: await sha256("dummy-owner-token"), secretsEnc,
956
+ config: { UPSTREAM_URL: "https://upstream.example.com/a2a", UPSTREAM_TOKEN: "plaintext-must-not-win", WAKE_WEBHOOK_URL: "https://hooks.example.com/wake" },
957
+ });
958
+ assert.equal((await push(1, sealed)).applied, true);
959
+ const facade = await stub.owner({ accountId: "dummy-owner", method: "GET", path: "/owner/facade" });
960
+ assert.equal(facade.status, 200);
961
+ assert.equal((facade.body as { hasUpstreamToken: boolean }).hasUpstreamToken, true);
962
+ assert.equal((facade.body as { hasUpstreamAccessServiceToken: boolean }).hasUpstreamAccessServiceToken, true);
963
+ assert.ok(received.length > 0);
964
+ for (const request of received) assert.deepEqual(request, { origin: "https://upstream.example.com", bearer: "Bearer dummy-bearer", id: "dummy-upstream-id", secret: "dummy-upstream-secret" });
965
+ const preview = await stub.owner({ accountId: "dummy-owner", method: "GET", path: "/owner/wake/preview" });
966
+ assert.equal(preview.status, 200);
967
+ assert.equal((preview.body as { hasAccessServiceToken: boolean }).hasAccessServiceToken, true);
968
+ for (const value of Object.values(secrets)) assert.equal(JSON.stringify([facade.body, preview.body]).includes(value), false);
969
+ const otherTenantBlob = await sealWakeSecrets("platform-secret", "another-tenant", secrets);
970
+ assert.ok(otherTenantBlob);
971
+ assert.equal((await push(2, otherTenantBlob)).applied, true);
972
+ const rejected = await stub.owner({ accountId: "dummy-owner", method: "GET", path: "/owner/facade" });
973
+ assert.equal((rejected.body as { hasUpstreamToken: boolean }).hasUpstreamToken, false);
974
+ assert.equal((rejected.body as { hasUpstreamAccessServiceToken: boolean }).hasUpstreamAccessServiceToken, false);
975
+ const emptyPreview = await stub.owner({ accountId: "dummy-owner", method: "GET", path: "/owner/wake/preview" });
976
+ assert.equal((emptyPreview.body as { hasAccessServiceToken: boolean }).hasAccessServiceToken, false);
977
+ assert.equal(received.at(-1)?.bearer, null, "an unreadable blob never falls back to plaintext");
978
+ });
@@ -329,7 +329,7 @@ test("scope: the MCP token is not an owner or peer token, and peer/owner tokens
329
329
  assert.equal((await s.call("GET", "/mcp", { headers: { authorization: `Bearer ${a.access_token}` } })).status, 405);
330
330
  // tools can't administer tokens or pairings
331
331
  const names = (await s.mcp(a.access_token, "tools/list")).data.result.tools.map((x: any) => x.name);
332
- assert.deepEqual(names, ["inbox", "show_task", "history", "mark_working", "reply", "send", "poll_outbound", "list_peers", "pairing_requests", "send_feedback"]);
332
+ assert.deepEqual(names, ["inbox", "show_task", "history", "mark_working", "reply", "send", "poll_outbound", "list_peers", "announce_address", "pairing_requests", "send_feedback"]);
333
333
  assert.ok(!names.some((n: string) => /approve|deny|token|revoke|issue|password/.test(n)));
334
334
  const unknown = await s.mcp(a.access_token, "tools/call", { name: "approve_pairing", arguments: { code: "X" } });
335
335
  assert.equal(unknown.data.error.code, -32602);
@@ -59,6 +59,10 @@ test("fresh database: every migration applies in order; a second run applies not
59
59
  assert.deepEqual(applyMigrations(db), [...FILES].sort(byNumber));
60
60
  for (const t of ["peers", "tasks", "wakes", "wake_budget", "device_requests", "pairing_rate", "settings", "facade_owners", "oauth_clients", "oauth_codes", "mcp_grants", "outbound_peers", "oauth_client_metadata", "oidc_txns", "conversation_owners", "reply_deliveries"]) assert.ok(tables(db).includes(t), t);
61
61
  assert.ok(columns(db, "peers").includes("source"));
62
+ assert.ok(columns(db, "peers").includes("identity_key"));
63
+ assert.ok(columns(db, "peers").includes("card_url"));
64
+ assert.ok(columns(db, "peers").includes("token_enc"));
65
+ assert.ok(columns(db, "device_requests").includes("identity_key"));
62
66
  assert.ok(columns(db, "device_requests").includes("replaces_label"));
63
67
  assert.ok(columns(db, "device_requests").includes("replaces_hash"));
64
68
  assert.deepEqual(applyMigrations(db), []);
@@ -67,15 +71,15 @@ test("fresh database: every migration applies in order; a second run applies not
67
71
  test("database from the 0002 fix (0001-0003 recorded): only 0004 and later apply; pending requests keep working", () => {
68
72
  const db = existingDb([sql("0001_init.sql"), sql("0002_wake_budget.sql"), sql("0003_device_pairing.sql")], ["0001_init.sql", "0002_wake_budget.sql", "0003_device_pairing.sql"]);
69
73
  db.prepare("INSERT INTO device_requests (device_hash, user_code, status, created_ms, expires_ms, interval_s) VALUES ('h', 'WDJB4827', 'pending', 1, 2, 5)").run();
70
- assert.deepEqual(applyMigrations(db), ["0004_pairing_replace.sql", "0005_facade_owners.sql", "0006_mcp.sql", "0007_cimd.sql", "0008_oidc_txns.sql", "0009_outbound_seen.sql", "0010_conversation_peer.sql", "0011_conversation_owner.sql", "0012_conversation_peer_owner.sql", "0013_reply_delivery.sql"]);
74
+ assert.deepEqual(applyMigrations(db), ["0004_pairing_replace.sql", "0005_facade_owners.sql", "0006_mcp.sql", "0007_cimd.sql", "0008_oidc_txns.sql", "0009_outbound_seen.sql", "0010_conversation_peer.sql", "0011_conversation_owner.sql", "0012_conversation_peer_owner.sql", "0013_reply_delivery.sql", "0014_peer_address.sql", "0015_outbound_source.sql"]);
71
75
  assert.equal((db.prepare("SELECT replaces_label FROM device_requests WHERE device_hash = 'h'").get() as any).replaces_label, null);
72
76
  });
73
77
 
74
78
  test("database from v0.2.0 (0001 and 0003 recorded): 0002 applies as a no-op and keeps the data", () => {
75
79
  const db = existingDb([sql("0001_init.sql"), sql("0003_device_pairing.sql")], ["0001_init.sql", "0003_device_pairing.sql"]);
76
80
  db.prepare("INSERT INTO wake_budget (hour, count) VALUES (1, 7)").run();
77
- assert.deepEqual(applyMigrations(db), ["0002_wake_budget.sql", "0004_pairing_replace.sql", "0005_facade_owners.sql", "0006_mcp.sql", "0007_cimd.sql", "0008_oidc_txns.sql", "0009_outbound_seen.sql", "0010_conversation_peer.sql", "0011_conversation_owner.sql", "0012_conversation_peer_owner.sql", "0013_reply_delivery.sql"]);
78
- assert.deepEqual(recorded(db), ["0001_init.sql", "0003_device_pairing.sql", "0002_wake_budget.sql", "0004_pairing_replace.sql", "0005_facade_owners.sql", "0006_mcp.sql", "0007_cimd.sql", "0008_oidc_txns.sql", "0009_outbound_seen.sql", "0010_conversation_peer.sql", "0011_conversation_owner.sql", "0012_conversation_peer_owner.sql", "0013_reply_delivery.sql"]);
81
+ assert.deepEqual(applyMigrations(db), ["0002_wake_budget.sql", "0004_pairing_replace.sql", "0005_facade_owners.sql", "0006_mcp.sql", "0007_cimd.sql", "0008_oidc_txns.sql", "0009_outbound_seen.sql", "0010_conversation_peer.sql", "0011_conversation_owner.sql", "0012_conversation_peer_owner.sql", "0013_reply_delivery.sql", "0014_peer_address.sql", "0015_outbound_source.sql"]);
82
+ assert.deepEqual(recorded(db), ["0001_init.sql", "0003_device_pairing.sql", "0002_wake_budget.sql", "0004_pairing_replace.sql", "0005_facade_owners.sql", "0006_mcp.sql", "0007_cimd.sql", "0008_oidc_txns.sql", "0009_outbound_seen.sql", "0010_conversation_peer.sql", "0011_conversation_owner.sql", "0012_conversation_peer_owner.sql", "0013_reply_delivery.sql", "0014_peer_address.sql", "0015_outbound_source.sql"]);
79
83
  assert.equal((db.prepare("SELECT count FROM wake_budget WHERE hour = 1").get() as any).count, 7);
80
84
  });
81
85
 
@@ -107,7 +111,7 @@ test("database from an earlier build without wake_budget (0001 and 0003 recorded
107
111
  assert.equal(before.data.error?.code, -32603, JSON.stringify(before.data));
108
112
  assert.equal(wakes.length, 0);
109
113
 
110
- assert.deepEqual(applyMigrations(db), ["0002_wake_budget.sql", "0004_pairing_replace.sql", "0005_facade_owners.sql", "0006_mcp.sql", "0007_cimd.sql", "0008_oidc_txns.sql", "0009_outbound_seen.sql", "0010_conversation_peer.sql", "0011_conversation_owner.sql", "0012_conversation_peer_owner.sql", "0013_reply_delivery.sql"]);
114
+ assert.deepEqual(applyMigrations(db), ["0002_wake_budget.sql", "0004_pairing_replace.sql", "0005_facade_owners.sql", "0006_mcp.sql", "0007_cimd.sql", "0008_oidc_txns.sql", "0009_outbound_seen.sql", "0010_conversation_peer.sql", "0011_conversation_owner.sql", "0012_conversation_peer_owner.sql", "0013_reply_delivery.sql", "0014_peer_address.sql", "0015_outbound_source.sql"]);
111
115
  assert.deepEqual(columns(db, "wake_budget"), ["hour", "count"]);
112
116
  assert.deepEqual(applyMigrations(db), []);
113
117
 
@@ -169,3 +173,16 @@ test("0012 lets each peer own the same context id and keeps the first row", () =
169
173
  const peers = db.prepare("SELECT peer FROM conversation_owners WHERE context_id = 'ctx-y' ORDER BY peer").all() as { peer: string }[];
170
174
  assert.deepEqual(peers.map((r) => r.peer), ["ada", "bob"]);
171
175
  });
176
+
177
+ test("0014 adds address columns and keeps an existing peer token", () => {
178
+ const prior = [...FILES].sort(byNumber).filter((f) => f !== "0014_peer_address.sql");
179
+ const db = existingDb(prior.map(sql), prior);
180
+ db.prepare("INSERT INTO peers (label, token_hash, created_at, source, user_code, client_name) VALUES ('ada', 'hash', '2026-01-01T00:00:00.000Z', 'pairing', 'WDJB4827', 'Ada')").run();
181
+ assert.deepEqual(applyMigrations(db), ["0014_peer_address.sql"]);
182
+ const row = db.prepare("SELECT token_hash, identity_key, card_url, token_enc FROM peers WHERE label = 'ada'").get() as { token_hash: string; identity_key: string | null; card_url: string | null; token_enc: string | null };
183
+ assert.equal(row.token_hash, "hash");
184
+ assert.equal(row.identity_key, null);
185
+ assert.equal(row.card_url, null);
186
+ assert.equal(row.token_enc, null);
187
+ assert.ok(columns(db, "device_requests").includes("identity_key"));
188
+ });
@@ -20,15 +20,25 @@ async function fixture(t: Parameters<typeof test>[1] extends (t: infer T) => unk
20
20
  };
21
21
  const byId = new Map<string, TenantContext>();
22
22
  let directPosts = 0;
23
+ const cards: string[] = [];
24
+ let missingCard = false;
23
25
  let hold: Promise<void> | null = null;
26
+ let posted: (() => void) | null = null;
27
+ let aborted = false;
24
28
  const ns: DoNamespace = {
25
29
  idFromName(name: string) { return name; },
26
30
  get(id: unknown) {
27
31
  return { async fetch(req: Request) {
28
32
  const ctx = byId.get(String(id));
29
33
  if (!ctx) return new Response("missing", { status: 404 });
34
+ if (req.method === "GET" && new URL(req.url).pathname.startsWith("/.well-known/agent")) {
35
+ cards.push(new URL(req.url).pathname);
36
+ if (missingCard) return new Response("missing", { status: 404 });
37
+ }
30
38
  if (req.method === "POST" && new URL(req.url).pathname === "/") {
31
39
  directPosts++;
40
+ posted?.();
41
+ req.signal.addEventListener("abort", () => { aborted = true; }, { once: true });
32
42
  if (hold) await hold;
33
43
  }
34
44
  return dispatch(req, ctx, ectx as never);
@@ -61,10 +71,11 @@ async function fixture(t: Parameters<typeof test>[1] extends (t: infer T) => unk
61
71
  assert.equal(r.status, 200, JSON.stringify(data));
62
72
  return data.result.id as string;
63
73
  };
64
- return { a, b, dbA, dbB, owner, sendToBob, get directPosts() { return directPosts; }, blockDirect() {
74
+ return { a, b, dbA, dbB, owner, sendToBob, cards, missingCard() { missingCard = true; }, get directPosts() { return directPosts; }, get aborted() { return aborted; }, blockDirect() {
65
75
  let release!: () => void;
66
76
  hold = new Promise<void>((resolve) => { release = resolve; });
67
- return () => { hold = null; release(); };
77
+ const started = new Promise<void>((resolve) => { posted = resolve; });
78
+ return Object.assign(() => { hold = null; posted = null; release(); }, { started });
68
79
  } };
69
80
  }
70
81
 
@@ -129,8 +140,9 @@ test("stale recovery never overlaps an uncertain delivery and a later retry read
129
140
  const s = await fixture(t as never);
130
141
  const taskId = await s.sendToBob("concurrent question", "ctx-concurrent");
131
142
  const release = s.blockDirect();
143
+ t.after(release);
132
144
  const first = s.owner(s.b, "acct-bob", "POST", "/owner/mcp/reply", { taskId, text: "one answer" });
133
- while (s.directPosts === 0) await new Promise((resolve) => setTimeout(resolve, 0));
145
+ await release.started;
134
146
  await s.dbB.prepare("UPDATE reply_deliveries SET updated_at = '2000-01-01T00:00:00.000Z'").run();
135
147
  const raced = await s.owner(s.b, "acct-bob", "POST", "/owner/mcp/reply", { taskId, text: "one answer" });
136
148
  assert.equal(raced.status, 200);
@@ -138,11 +150,48 @@ test("stale recovery never overlaps an uncertain delivery and a later retry read
138
150
  assert.match(String((raced.body as any).structuredContent.detail), /uncertain.*not sent again/);
139
151
  assert.equal(s.directPosts, 1, "the overlapping call cannot start a second delivery");
140
152
  release();
141
- assert.equal(((await first).body as any).structuredContent.status, "delivered");
153
+ assert.deepEqual(((await first).body as any).structuredContent, (raced.body as any).structuredContent, "a late success cannot replace the recorded uncertain result");
142
154
  const retry = await s.owner(s.b, "acct-bob", "POST", "/owner/mcp/reply", { taskId, text: "one answer" });
143
155
  assert.equal(retry.status, 200);
144
- assert.equal((retry.body as any).structuredContent.status, "delivered");
156
+ assert.deepEqual((retry.body as any).structuredContent, (raced.body as any).structuredContent);
157
+ assert.equal(s.directPosts, 1);
158
+ });
159
+
160
+ test("an identical retry during delivery reports in-flight before the stored completed state", async (t) => {
161
+ const s = await fixture(t as never);
162
+ const taskId = await s.sendToBob("retry question", "ctx-in-flight");
163
+ const release = s.blockDirect();
164
+ t.after(release);
165
+ const first = s.owner(s.b, "acct-bob", "POST", "/owner/mcp/reply", { taskId, text: "one answer" });
166
+ await release.started;
167
+ const retry = await s.owner(s.b, "acct-bob", "POST", "/owner/mcp/reply", { taskId, text: "one answer" });
168
+ assert.equal(retry.status, 200);
169
+ assert.equal((retry.body as any).isError, true);
170
+ assert.match(String((retry.body as any).text), /already being delivered/);
171
+ assert.equal(s.directPosts, 1);
172
+ release();
173
+ assert.equal(((await first).body as any).structuredContent.status, "delivered");
174
+ });
175
+
176
+ test("the total delivery deadline returns while a peer ignores cancellation and retries never resend", async (t) => {
177
+ const s = await fixture(t as never);
178
+ const taskId = await s.sendToBob("slow question", "ctx-deadline");
179
+ const release = s.blockDirect();
180
+ t.after(release);
181
+ t.mock.timers.enable({ apis: ["setTimeout"] });
182
+ t.after(() => t.mock.timers.reset());
183
+ const first = s.owner(s.b, "acct-bob", "POST", "/owner/mcp/reply", { taskId, text: "bounded answer" });
184
+ await release.started;
185
+ t.mock.timers.tick(18000);
186
+ t.mock.timers.reset();
187
+ const result = await first;
188
+ assert.equal(result.status, 200);
189
+ assert.equal((result.body as any).structuredContent.status, "delivery-failed");
190
+ assert.equal(s.aborted, true, "the deadline reaches the actual peer request signal");
191
+ const retry = await s.owner(s.b, "acct-bob", "POST", "/owner/mcp/reply", { taskId, text: "bounded answer" });
192
+ assert.deepEqual((retry.body as any).structuredContent, (result.body as any).structuredContent);
145
193
  assert.equal(s.directPosts, 1);
194
+ release();
146
195
  });
147
196
 
148
197
  test("a remote token rejection reports HTTP status without losing the stored reply", async (t) => {
@@ -159,3 +208,14 @@ test("a remote token rejection reports HTTP status without losing the stored rep
159
208
  assert.equal(stored.state, "completed");
160
209
  assert.match(stored.status_json, /the answer stays/);
161
210
  });
211
+
212
+
213
+ test("reply discovers one canonical card and uses the paired base when it is missing", async (t) => {
214
+ const s = await fixture(t as never);
215
+ const taskId = await s.sendToBob("no card question", "ctx-one-card");
216
+ s.missingCard();
217
+ const result = await s.owner(s.b, "acct-bob", "POST", "/owner/mcp/reply", { taskId, text: "stored answer" });
218
+ assert.equal((result.body as any).structuredContent.status, "delivered");
219
+ assert.deepEqual(s.cards, ["/.well-known/agent-card.json"]);
220
+ assert.equal(s.directPosts, 1);
221
+ });
@@ -320,3 +320,13 @@ test("public-https does not request a metadata address, and does post to a check
320
320
  assert.equal(tls.status, null);
321
321
  assert.equal(tls.info, "Error: TLS Handshake Failed.");
322
322
  });
323
+
324
+
325
+ test("an aborted peer request starts neither DNS lookup nor delivery", async () => {
326
+ const controller = new AbortController();
327
+ controller.abort(new Error("delivery deadline"));
328
+ await assert.rejects(() => fetchCheckedPeer("https://hook.example/rpc", { signal: controller.signal }, {
329
+ async lookup() { assert.fail("aborted request must not resolve DNS"); },
330
+ async deliver() { assert.fail("aborted request must not send"); },
331
+ }), /delivery deadline/);
332
+ });