-
Notifications
You must be signed in to change notification settings - Fork 23
Serve website A2A handle routes #246
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export { GET } from "../route"; | ||
|
|
||
| export const runtime = "nodejs"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import { | ||
| agentCardResponse, | ||
| notFound, | ||
| resolveA2aAgentCard, | ||
| } from "@src/common/a2a-route"; | ||
|
|
||
| type RouteContext = { | ||
| params: Promise<{ | ||
| id: string; | ||
| }>; | ||
| }; | ||
|
|
||
| export const runtime = "nodejs"; | ||
|
|
||
| export async function GET( | ||
| _request: Request, | ||
| { params }: RouteContext | ||
| ): Promise<Response> { | ||
| const { id } = await params; | ||
| const card = await resolveA2aAgentCard(id); | ||
| return card ? agentCardResponse(card) : notFound(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import { | ||
| agentDocResponse, | ||
| notFound, | ||
| resolveA2aAgentCard, | ||
| } from "@src/common/a2a-route"; | ||
|
|
||
| type RouteContext = { | ||
| params: Promise<{ | ||
| id: string; | ||
| }>; | ||
| }; | ||
|
|
||
| export const runtime = "nodejs"; | ||
|
|
||
| export async function GET( | ||
| _request: Request, | ||
| { params }: RouteContext | ||
| ): Promise<Response> { | ||
| const { id } = await params; | ||
| const card = await resolveA2aAgentCard(id); | ||
| return card ? agentDocResponse(card, "skillMd") : notFound(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import { | ||
| agentDocResponse, | ||
| notFound, | ||
| resolveA2aAgentCard, | ||
| } from "@src/common/a2a-route"; | ||
|
|
||
| type RouteContext = { | ||
| params: Promise<{ | ||
| id: string; | ||
| }>; | ||
| }; | ||
|
|
||
| export const runtime = "nodejs"; | ||
|
|
||
| export async function GET( | ||
| _request: Request, | ||
| { params }: RouteContext | ||
| ): Promise<Response> { | ||
| const { id } = await params; | ||
| const card = await resolveA2aAgentCard(id); | ||
| return card ? agentDocResponse(card, "swaggerJson") : notFound(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import { | ||
| agentDocResponse, | ||
| notFound, | ||
| resolveA2aAgentCard, | ||
| } from "@src/common/a2a-route"; | ||
|
|
||
| type RouteContext = { | ||
| params: Promise<{ | ||
| id: string; | ||
| }>; | ||
| }; | ||
|
|
||
| export const runtime = "nodejs"; | ||
|
|
||
| export async function GET( | ||
| _request: Request, | ||
| { params }: RouteContext | ||
| ): Promise<Response> { | ||
| const { id } = await params; | ||
| const card = await resolveA2aAgentCard(id); | ||
| return card ? agentDocResponse(card, "swaggerMd") : notFound(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import { describe, expect, it, vi } from "vitest"; | ||
|
|
||
| import { | ||
| type A2aAgentCard, | ||
| agentDocResponse, | ||
| agentDocUrl, | ||
| resolveA2aAgentCard, | ||
| } from "./a2a-route"; | ||
|
|
||
| const baseCard: A2aAgentCard = { | ||
| agentId: "A8sVmcaC5apxoUx1kCA4pPa9RttQK1HXmfkziSFf5dVg", | ||
| createdAt: "2026-07-11T00:00:00Z", | ||
| cryptoId: "A8sVmcaC5apxoUx1kCA4pPa9RttQK1HXmfkziSFf5dVg", | ||
| name: "NatureDesk", | ||
| updatedAt: "2026-07-11T00:00:00Z", | ||
| url: "https://naturedesk.github.io/site/a2a/@naturedesk/agent-card.json", | ||
| }; | ||
|
|
||
| describe("A2A route helpers", () => { | ||
| it("resolves @handles through the directory before fetching the agent card", async () => { | ||
| const fetcher = vi.fn(async (input: RequestInfo | URL) => { | ||
| const url = String(input); | ||
| if (url.endsWith("/directory/resolve/%40naturedesk")) { | ||
| return Response.json({ | ||
| identity: { cryptoId: baseCard.agentId }, | ||
| }); | ||
| } | ||
| if (url.endsWith(`/directory/agents/${baseCard.agentId}`)) { | ||
| return Response.json(baseCard); | ||
| } | ||
| return Response.json({ error: "unexpected" }, { status: 500 }); | ||
| }); | ||
|
|
||
| await expect( | ||
| resolveA2aAgentCard( | ||
| "@naturedesk", | ||
| fetcher as typeof fetch, | ||
| "https://api.example.test" | ||
| ) | ||
| ).resolves.toMatchObject({ name: "NatureDesk" }); | ||
| expect(fetcher).toHaveBeenCalledTimes(2); | ||
| }); | ||
|
|
||
| it("derives docs from an external agent-card URL when no docs URL is advertised", () => { | ||
| expect(agentDocUrl(baseCard, "skillMd")).toBe( | ||
| "https://naturedesk.github.io/site/a2a/@naturedesk/skill.md" | ||
| ); | ||
| }); | ||
|
|
||
| it("redirects to advertised skill markdown without proxying it", async () => { | ||
| const card: A2aAgentCard = { | ||
| ...baseCard, | ||
| docs: { | ||
| skillMdUrl: "https://docs.example.test/skill.md", | ||
| }, | ||
| }; | ||
| const fetcher = vi.fn(); | ||
|
|
||
| const response = await agentDocResponse(card, "skillMd", fetcher as typeof fetch); | ||
| expect(response.status).toBe(302); | ||
| expect(response.headers.get("Location")).toBe( | ||
| "https://docs.example.test/skill.md" | ||
| ); | ||
| expect(fetcher).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("redirects to relative advertised docs URLs", async () => { | ||
| const card: A2aAgentCard = { | ||
| ...baseCard, | ||
| docs: { | ||
| skillMdUrl: "/a2a/@naturedesk/skill.md", | ||
| }, | ||
| }; | ||
| const fetcher = vi.fn(); | ||
|
|
||
| expect(agentDocUrl(card, "skillMd")).toBe("/a2a/@naturedesk/skill.md"); | ||
| const response = await agentDocResponse(card, "skillMd", fetcher as typeof fetch); | ||
| expect(response.status).toBe(302); | ||
| expect(response.headers.get("Location")).toBe("/a2a/@naturedesk/skill.md"); | ||
| expect(fetcher).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("serves inline skill markdown without fetching", async () => { | ||
| const card: A2aAgentCard = { | ||
| ...baseCard, | ||
| docs: { | ||
| skillMd: "# Inline skill\n", | ||
| }, | ||
| }; | ||
| const fetcher = vi.fn(); | ||
|
|
||
| const response = await agentDocResponse(card, "skillMd", fetcher as typeof fetch); | ||
| await expect(response.text()).resolves.toBe("# Inline skill\n"); | ||
| expect(fetcher).not.toHaveBeenCalled(); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,188 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type Fetcher = typeof fetch; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const A2A_API_BASE_URL = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.env["NEXT_PUBLIC_API_BASE_URL"] ?? "https://staging-api.tiny.place"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export type A2aAgentCard = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| agentId: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cryptoId: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| docs?: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| skillMd?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| skillMdUrl?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| swaggerJson?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| swaggerJsonUrl?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| swaggerMd?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| swaggerMdUrl?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [key: string]: unknown; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type ResolveResponse = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| agent?: A2aAgentCard | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| identity?: { cryptoId?: string | null } | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type DocKind = "skillMd" | "swaggerJson" | "swaggerMd"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const DOC_FILENAMES: Record<DocKind, string> = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| skillMd: "skill.md", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| swaggerJson: "swagger.json", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| swaggerMd: "swagger.md", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const DOC_CONTENT_TYPES: Record<DocKind, string> = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| skillMd: "text/markdown; charset=utf-8", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| swaggerJson: "application/json; charset=utf-8", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| swaggerMd: "text/markdown; charset=utf-8", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function resolveA2aAgentCard( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fetcher: Fetcher = fetch, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| apiBaseUrl = A2A_API_BASE_URL | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<A2aAgentCard | null> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const normalized = decodeRouteId(id); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (normalized === "") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (normalized.startsWith("@")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const resolved = await fetchJson<ResolveResponse>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `${apiBaseUrl}/directory/resolve/${encodeURIComponent(normalized)}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fetcher | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!resolved) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (resolved.agent) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return resolved.agent; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cryptoId = resolved.identity?.cryptoId; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return cryptoId ? fetchAgentCard(cryptoId, fetcher, apiBaseUrl) : null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return fetchAgentCard(normalized, fetcher, apiBaseUrl); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function agentCardResponse(card: A2aAgentCard): Response { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Response.json(card, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Cache-Control": "public, s-maxage=60, stale-while-revalidate=300", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function agentDocResponse( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| card: A2aAgentCard, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| kind: DocKind, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _fetcher: Fetcher = fetch | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<Response> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const inline = inlineDoc(card, kind); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (inline !== undefined) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new Response(inline, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Cache-Control": "public, s-maxage=60, stale-while-revalidate=300", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Content-Type": DOC_CONTENT_TYPES[kind], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const url = agentDocUrl(card, kind); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!url) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return notFound(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new Response(null, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: 302, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Cache-Control": "public, s-maxage=60, stale-while-revalidate=300", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Location: url, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+92
to
+103
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Self-referential redirect can cause infinite redirect loops. If an agent card advertises a doc URL that resolves to the current route (e.g., Consider passing the request URL to 🔧 Proposed approach export async function agentDocResponse(
card: A2aAgentCard,
kind: DocKind,
- _fetcher: Fetcher = fetch
+ _fetcher: Fetcher = fetch,
+ requestUrl?: string
): Promise<Response> {
const inline = inlineDoc(card, kind);
if (inline !== undefined) {
return new Response(inline, {
headers: {
"Cache-Control": "public, s-maxage=60, stale-while-revalidate=300",
"Content-Type": DOC_CONTENT_TYPES[kind],
},
});
}
const url = agentDocUrl(card, kind);
if (!url) {
return notFound();
}
+ if (requestUrl && resolvesToSameUrl(url, requestUrl)) {
+ return notFound();
+ }
+
return new Response(null, {
status: 302,
headers: {
"Cache-Control": "public, s-maxage=60, stale-while-revalidate=300",
Location: url,
},
});
}Then update route handlers to pass the request URL: export async function GET(
- _request: Request,
+ request: Request,
{ params }: RouteContext
): Promise<Response> {
const { id } = await params;
const card = await resolveA2aAgentCard(id);
- return card ? agentDocResponse(card, "skillMd") : notFound();
+ return card ? agentDocResponse(card, "skillMd", fetch, request.url) : notFound();
}🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function agentDocUrl(card: A2aAgentCard, kind: DocKind): string | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const docs = card.docs; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const urlField = `${kind}Url` as keyof NonNullable<A2aAgentCard["docs"]>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const advertisedUrl = docs?.[urlField]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof advertisedUrl === "string" && isDocUrl(advertisedUrl)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return advertisedUrl; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const advertisedValue = docs?.[kind]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof advertisedValue === "string" && isDocUrl(advertisedValue)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return advertisedValue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (card.url && isHttpUrl(card.url)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new URL(DOC_FILENAMES[kind], card.url).toString(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function notFound(): Response { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new Response("Not found", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: 404, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { "Content-Type": "text/plain; charset=utf-8" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function inlineDoc(card: A2aAgentCard, kind: DocKind): string | undefined { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const value = card.docs?.[kind]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof value === "string" && !isDocUrl(value)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function fetchAgentCard( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| agentId: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fetcher: Fetcher, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| apiBaseUrl: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<A2aAgentCard | null> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return fetchJson<A2aAgentCard>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `${apiBaseUrl}/directory/agents/${encodeURIComponent(agentId)}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fetcher | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function fetchJson<T>(url: string, fetcher: Fetcher): Promise<T | null> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const response = await fetcher(url, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { Accept: "application/json" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (response.status === 404) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!response.ok) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`A2A directory lookup failed: HTTP ${response.status}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return (await response.json()) as T; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+152
to
+163
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🛡️ Proposed fix async function fetchJson<T>(url: string, fetcher: Fetcher): Promise<T | null> {
- const response = await fetcher(url, {
- headers: { Accept: "application/json" },
- });
- if (response.status === 404) {
- return null;
- }
- if (!response.ok) {
- throw new Error(`A2A directory lookup failed: HTTP ${response.status}`);
- }
- return (await response.json()) as T;
+ try {
+ const response = await fetcher(url, {
+ headers: { Accept: "application/json" },
+ });
+ if (response.status === 404) {
+ return null;
+ }
+ if (!response.ok) {
+ throw new Error(`A2A directory lookup failed: HTTP ${response.status}`);
+ }
+ return (await response.json()) as T;
+ } catch (error) {
+ if (error instanceof Error && error.message.startsWith("A2A directory")) {
+ throw error;
+ }
+ return null;
+ }
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function decodeRouteId(id: string): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return decodeURIComponent(id).trim(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return id.trim(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function isHttpUrl(value: string): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const url = new URL(value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return url.protocol === "http:" || url.protocol === "https:"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function isDocUrl(value: string): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return isHttpUrl(value) || isRelativePath(value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function isRelativePath(value: string): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return value.startsWith("/") && !value.startsWith("//"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
GET handler lacks try-catch; errors from
resolveA2aAgentCardproduce unhandled 500s.If
resolveA2aAgentCardthrows (network error, malformed JSON from directory API), the error propagates unhandled. Per coding guidelines, all async functions must have try-catch. The same issue exists inskill.md/route.ts,swagger.json/route.ts, andswagger.md/route.ts.🛡️ Proposed fix
export async function GET( _request: Request, { params }: RouteContext ): Promise<Response> { - const { id } = await params; - const card = await resolveA2aAgentCard(id); - return card ? agentCardResponse(card) : notFound(); + try { + const { id } = await params; + const card = await resolveA2aAgentCard(id); + return card ? agentCardResponse(card) : notFound(); + } catch { + return new Response("Internal server error", { status: 500 }); + } }📝 Committable suggestion
🤖 Prompt for AI Agents
Source: Coding guidelines