Fix agent-auth review issues and docs build

- Grants no longer expire at the session TTL (expiresAt: null); agent
  lifetime clocks govern access, so long-lived agents keep their grants.
- list_users rejects an over-max limit with constraint_violated instead
  of silently clamping (fail loud); default limit stays bounded by max.
- Discovery route auth schema uses adaptSchema for type-correct build.
- Add missing agent-auth entry to docs APP_ICONS Record<AppId> map,
  which was breaking @hexclave/docs build (and build/lint/docker CI).

Co-Authored-By: madison@stack-auth.com <madison.w.kennedy@gmail.com>
This commit is contained in:
Devin AI 2026-07-07 23:49:13 +00:00
parent 5e6e1165c3
commit e26f326d8c
7 changed files with 50 additions and 17 deletions

View File

@ -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({

View File

@ -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),
},

View File

@ -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,

View File

@ -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),
},

View File

@ -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<TInput extends Record<string, unknown>>(

View File

@ -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",

View File

@ -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<Record<AppId, string>> = {
// Icon mapping for docs (no Next.js Image dependencies)
const APP_ICONS: Record<AppId, React.FunctionComponent<React.SVGProps<SVGSVGElement>>> = {
authentication: ShieldEllipsis,
"agent-auth": Fingerprint,
teams: Users,
rbac: UserCog,
"api-keys": KeyRound,