diff --git a/apps/backend/src/app/.well-known/agent-configuration/route.ts b/apps/backend/src/app/.well-known/agent-configuration/route.ts index 3e0ea7675..9a38eb3e5 100644 --- a/apps/backend/src/app/.well-known/agent-configuration/route.ts +++ b/apps/backend/src/app/.well-known/agent-configuration/route.ts @@ -1,5 +1,5 @@ import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; -import { jsonSchema, yupNumber, yupObject, yupString } from "@hexclave/shared/dist/schema-fields"; +import { adaptSchema, jsonSchema, yupNumber, yupObject, yupString } from "@hexclave/shared/dist/schema-fields"; import { StatusError } from "@hexclave/shared/dist/utils/errors"; import { AGENT_AUTH_AGENTS_PATH, AGENT_AUTH_CAPABILITY_EXECUTE_PATH, AGENT_AUTH_DISCOVERY_PATH, AGENT_AUTH_MODES, AGENT_AUTH_PROTOCOL_VERSION, AGENT_AUTH_REGISTER_PATH } from "@/lib/agent-auth/constants"; import { AGENT_CAPABILITIES } from "@/lib/agent-auth/capabilities"; @@ -12,9 +12,9 @@ function buildDiscoveryUrl(baseUrl: string, path: string) { export const GET = createSmartRouteHandler({ request: yupObject({ auth: yupObject({ - type: yupString(), - user: jsonSchema, - project: jsonSchema, + type: adaptSchema, + user: adaptSchema, + project: adaptSchema, }).nullable(), }), response: yupObject({ diff --git a/apps/backend/src/app/api/latest/agent-auth/agents/[agentId]/route.ts b/apps/backend/src/app/api/latest/agent-auth/agents/[agentId]/route.ts index f9eaaa1f1..5dfefce65 100644 --- a/apps/backend/src/app/api/latest/agent-auth/agents/[agentId]/route.ts +++ b/apps/backend/src/app/api/latest/agent-auth/agents/[agentId]/route.ts @@ -42,8 +42,8 @@ export const GET = createSmartRouteHandler({ } return { - statusCode: 200 as const, - bodyType: "json" as const, + statusCode: 200, + bodyType: "json", body: { agent: serializeAgent(agent), }, diff --git a/apps/backend/src/app/api/latest/agent-auth/agents/register/route.ts b/apps/backend/src/app/api/latest/agent-auth/agents/register/route.ts index dd9f92952..b2e176b7e 100644 --- a/apps/backend/src/app/api/latest/agent-auth/agents/register/route.ts +++ b/apps/backend/src/app/api/latest/agent-auth/agents/register/route.ts @@ -134,7 +134,7 @@ export const POST = createSmartRouteHandler({ status: "ACTIVE", constraints: capability.constraints == null ? undefined : JSON.parse(JSON.stringify(capability.constraints)), grantedByProjectUserId: existingUser.projectUserId, - expiresAt: sessionExpiresAt, + expiresAt: null, })), }); } @@ -159,8 +159,8 @@ export const POST = createSmartRouteHandler({ }); return { - statusCode: 201 as const, - bodyType: "json" as const, + statusCode: 201, + bodyType: "json", body: { host_id: host.id, agent_id: agent.id, diff --git a/apps/backend/src/app/api/latest/agent-auth/agents/route.ts b/apps/backend/src/app/api/latest/agent-auth/agents/route.ts index 5583a0baa..bf79f19c1 100644 --- a/apps/backend/src/app/api/latest/agent-auth/agents/route.ts +++ b/apps/backend/src/app/api/latest/agent-auth/agents/route.ts @@ -32,8 +32,8 @@ export const GET = createSmartRouteHandler({ }); return { - statusCode: 200 as const, - bodyType: "json" as const, + statusCode: 200, + bodyType: "json", body: { agents: agents.map(serializeAgent), }, diff --git a/apps/backend/src/lib/agent-auth/capabilities.ts b/apps/backend/src/lib/agent-auth/capabilities.ts index fd344f3c5..71fd27620 100644 --- a/apps/backend/src/lib/agent-auth/capabilities.ts +++ b/apps/backend/src/lib/agent-auth/capabilities.ts @@ -104,9 +104,10 @@ function assertConstraintSatisfied(field: string, value: unknown, constraint: Ag } function applyConstraintsForListUsers(input: { limit?: number }, constraints: AgentCapabilityGrantConstraints | null | undefined) { + const requestedLimit = input.limit; if (constraints == null || Object.keys(constraints).length === 0) { return { - limit: input.limit ?? AGENT_AUTH_DEFAULT_LIST_USERS_LIMIT, + limit: requestedLimit ?? AGENT_AUTH_DEFAULT_LIST_USERS_LIMIT, }; } @@ -117,10 +118,25 @@ function applyConstraintsForListUsers(input: { limit?: number }, constraints: Ag } const limitConstraint = constraints.limit; - if (limitConstraint == null) return input; - const resolvedLimit = clampNumber(input.limit ?? AGENT_AUTH_DEFAULT_LIST_USERS_LIMIT, limitConstraint.min, limitConstraint.max); - assertConstraintSatisfied("limit", resolvedLimit, limitConstraint); - return { limit: resolvedLimit }; + if (limitConstraint == null) { + return { + limit: requestedLimit ?? AGENT_AUTH_DEFAULT_LIST_USERS_LIMIT, + }; + } + + if (requestedLimit != null) { + assertConstraintSatisfied("limit", requestedLimit, limitConstraint); + return { limit: requestedLimit }; + } + + if (limitConstraint.min != null && limitConstraint.max != null && limitConstraint.min > limitConstraint.max) { + throwConstraintViolation("constraint_violated"); + } + + const defaultLimit = AGENT_AUTH_DEFAULT_LIST_USERS_LIMIT; + const boundedLimit = clampNumber(defaultLimit, limitConstraint.min, limitConstraint.max); + assertConstraintSatisfied("limit", boundedLimit, limitConstraint); + return { limit: boundedLimit }; } export function validateAgentCapabilityInput>( diff --git a/apps/e2e/tests/backend/agent-auth.test.ts b/apps/e2e/tests/backend/agent-auth.test.ts index 79bd12e43..bde995e76 100644 --- a/apps/e2e/tests/backend/agent-auth.test.ts +++ b/apps/e2e/tests/backend/agent-auth.test.ts @@ -124,6 +124,22 @@ describe("agent auth", () => { expect(listUsersResponse.status).toBe(200); expect(listUsersResponse.body.result.users).toHaveLength(1); + const invalidListUsersResponse = await niceBackendFetch("/api/latest/agent-auth/capabilities/execute", { + accessType: "server", + method: "POST", + headers: { + authorization: `Bearer ${agentJwt}`, + }, + body: { + capability: "list_users", + input: { + limit: 50, + }, + }, + }); + expect(invalidListUsersResponse.status).toBe(400); + expect(invalidListUsersResponse.body).toBe("constraint_violated"); + const projectInfoResponse = await niceBackendFetch("/api/latest/agent-auth/capabilities/execute", { accessType: "server", method: "POST", diff --git a/docs/src/components/mdx/app-card.tsx b/docs/src/components/mdx/app-card.tsx index 93c3c4419..8da75a756 100644 --- a/docs/src/components/mdx/app-card.tsx +++ b/docs/src/components/mdx/app-card.tsx @@ -2,7 +2,7 @@ import { ALL_APPS, AppId } from "@hexclave/shared/dist/apps/apps-config"; import { AppIcon, appSquarePaddingExpression, appSquareWidthExpression } from "@hexclave/shared/dist/apps/apps-ui"; -import { BarChart3, ClipboardList, Code, CreditCard, Headset, KeyRound, Mail, Mails, MousePointerClick, PlayCircle, Rocket, ShieldCheck, ShieldEllipsis, Sparkles, Triangle, Tv, UserCog, Users, Vault, Webhook } from "lucide-react"; +import { BarChart3, ClipboardList, Code, CreditCard, Fingerprint, Headset, KeyRound, Mail, Mails, MousePointerClick, PlayCircle, Rocket, ShieldCheck, ShieldEllipsis, Sparkles, Triangle, Tv, UserCog, Users, Vault, Webhook } from "lucide-react"; import Link from "next/link"; import { cn } from "../../lib/cn"; @@ -16,6 +16,7 @@ const APP_URL_OVERRIDES: Partial> = { // Icon mapping for docs (no Next.js Image dependencies) const APP_ICONS: Record>> = { authentication: ShieldEllipsis, + "agent-auth": Fingerprint, teams: Users, rbac: UserCog, "api-keys": KeyRound,