From 3847ef7b2d3b434a52bcb658524018428b51ecb5 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 1 Aug 2026 04:24:20 +0000 Subject: [PATCH] T14 fixup: route ordering + UUID guard for /api/agents/:id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verification after commit d4c02a3 found Hono matching /:id BEFORE /activity when the bare /api/agents/activity request hit the API — Postgres returned a 500 cast error because id='activity' was not a valid UUID. Two changes: 1. Move GET /api/agents/activity registration to BEFORE GET /:id so Hono's matcher picks the static path before the param path. 2. Add a UUID-format guard at the top of GET /api/agents/:id — returns 404 when id is not a UUID. Defense in depth: prevents future 500s if a similarly-shaped static route collides with /:id. Parent: t_e1cbd87d --- apps/api/src/routes/agents.ts | 40 +++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/apps/api/src/routes/agents.ts b/apps/api/src/routes/agents.ts index d8792d5..7fb95d5 100644 --- a/apps/api/src/routes/agents.ts +++ b/apps/api/src/routes/agents.ts @@ -100,11 +100,35 @@ agentRoutes.post("/", async (c) => { } }); + +// GET /api/agents/activity — All activity (bare path, no agent filter) +agentRoutes.get("/activity", async (c) => { + try { + await requireAuth(c); + const items = await db.select() + .from(agentActivity) + .orderBy(desc(agentActivity.createdAt)) + .limit(100); + return c.json({ items, totalItems: items.length }); + } catch (error) { + if (error instanceof AuthError) return c.json({ error: { code: error.code, message: error.message } }, error.status as any); + console.error("[agents] GET /activity error:", error); + return c.json({ error: { code: "INTERNAL_ERROR", message: "Failed to get activity" } }, 500); + } +}); + + // GET /api/agents/:id — Read agentRoutes.get("/:id", async (c) => { try { await requireAuth(c); const id = c.req.param("id"); + // Guard: /:id must be a UUID. Hono matches /:id before /activity when the + // param path was registered first; without this guard we get a Postgres + // "invalid input syntax for type uuid" 500 on /api/agents/activity. + if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) { + return c.json({ error: { code: "NOT_FOUND", message: "Agent not found" } }, 404); + } const [agent] = await db.select().from(agents).where(eq(agents.id, id)).limit(1); if (!agent) return c.json({ error: { code: "NOT_FOUND", message: "Agent not found" } }, 404); return c.json(agent); @@ -224,22 +248,6 @@ agentRoutes.get("/:id/permissions", async (c) => { } }); -// GET /api/agents/activity — All activity (bare path, no agent filter) -agentRoutes.get("/activity", async (c) => { - try { - await requireAuth(c); - const items = await db.select() - .from(agentActivity) - .orderBy(desc(agentActivity.createdAt)) - .limit(100); - return c.json({ items, totalItems: items.length }); - } catch (error) { - if (error instanceof AuthError) return c.json({ error: { code: error.code, message: error.message } }, error.status as any); - console.error("[agents] GET /activity error:", error); - return c.json({ error: { code: "INTERNAL_ERROR", message: "Failed to get activity" } }, 500); - } -}); - // GET /api/agents/:id/activity — Agent activity log (or all if id=_all) agentRoutes.get("/:id/activity", async (c) => { try {