Slice
API/route-contract audit of server/routes/agentPersonalities.js write handlers vs server/lib/agentValidation.js (the schema already used by POST/PUT create-update). Audited 2026-09-11.
model:light because the Zod shapes already exist and only need wiring on two handlers; effort:low because the change is a small schema + validateRequest plus a couple of 400 cases.
Problem
Create and update already go through validateRequest(agentSchema) / agentUpdateSchema (server/routes/agentPersonalities.js:41, 51) using server/lib/agentValidation.js:60-76. Generate and toggle do not.
POST /api/agents/personalities/generate (server/routes/agentPersonalities.js:75-80) reads req.body directly:
const { seed = {}, providerId, model } = req.body;
const generated = await generateAgentPersonality(seed, providerId, model);
generateAgentPersonality (server/services/agentPersonalityGenerator.js:61-62) destructures seed and then calls .trim() / nested field reads:
seed: null → TypeError: Cannot destructure property 'name' of 'null' → raw 500.
seed: { personality: null } → default = {} does not apply (null is present) → personality.style throws → raw 500.
seed: { name: 123 } → name.trim is not a function → raw 500.
seed.name / description / promptPrefix have no .max(), unlike agentSchema (name max 100, description max 1000, promptPrefix max 2000). A huge seed is interpolated into the LLM prompt (server/services/agentPersonalityGenerator.js:79-122) and billed.
Client: apiPersonalities.generateAgentPersonality (client/src/services/apiPersonalities.js:21-23) posts { seed, providerId, model }; AgentList.jsx:90-104 always sends a structured object, so the UI is fine — the route is not.
POST /api/agents/personalities/:id/toggle (server/routes/agentPersonalities.js:84-88) passes req.body.enabled straight into toggleAgent → updateAgent(id, { enabled }) (server/services/agentPersonalities.js:140-141) with no boolean check. A string "false" is truthy and persists as enabled: "false". Later UI !agent.enabled (client/src/components/agents/AgentDetail.jsx:70) then mis-inverts. Create/update already require enabled: z.boolean().
GET / (server/routes/agentPersonalities.js:17-24) takes req.query.userId with no schema. An array query (?userId=a&userId=b) reaches getAgentsByUser.
Impact
- Malformed generate bodies 500 instead of 400
VALIDATION_ERROR, and skip the { error, code, timestamp } envelope the rest of this router uses via ServerError.
- Unbounded seed text is interpolated into a paid LLM call (
source: 'agent-personality-generation').
- Toggle can persist a non-boolean
enabled, so the next click and any if (agent.enabled) gate read the wrong consent state.
Fix
Add two small schemas next to agentSchema in server/lib/agentValidation.js (or inline in the route) and wire validateRequest:
agentGenerateSchema: { seed: agentSchema.partial().optional().default({}), providerId: z.string().min(1).max(128).nullable().optional(), model: z.string().min(1).max(300).nullable().optional() }.strict(). Reject seed: null (do not coerce). Nested personality / avatar must be objects or omitted, not null.
agentToggleSchema: { enabled: z.boolean() }.strict().
- Optional:
z.object({ userId: z.string().min(1).max(100).optional() }) on GET /.
Do not change the generate prompt or the stored personality shape. Keep throwing ServerError from the service for provider-unavailable; only the request body becomes 400.
Scope
small
Acceptance criteria
Slice
API/route-contract audit of
server/routes/agentPersonalities.jswrite handlers vsserver/lib/agentValidation.js(the schema already used by POST/PUT create-update). Audited 2026-09-11.model:lightbecause the Zod shapes already exist and only need wiring on two handlers;effort:lowbecause the change is a small schema +validateRequestplus a couple of 400 cases.Problem
Create and update already go through
validateRequest(agentSchema)/agentUpdateSchema(server/routes/agentPersonalities.js:41, 51) usingserver/lib/agentValidation.js:60-76. Generate and toggle do not.POST
/api/agents/personalities/generate(server/routes/agentPersonalities.js:75-80) readsreq.bodydirectly:generateAgentPersonality(server/services/agentPersonalityGenerator.js:61-62) destructuresseedand then calls.trim()/ nested field reads:seed: null→TypeError: Cannot destructure property 'name' of 'null'→ raw 500.seed: { personality: null }→ default= {}does not apply (null is present) →personality.stylethrows → raw 500.seed: { name: 123 }→name.trim is not a function→ raw 500.seed.name/description/promptPrefixhave no.max(), unlikeagentSchema(namemax 100,descriptionmax 1000,promptPrefixmax 2000). A huge seed is interpolated into the LLM prompt (server/services/agentPersonalityGenerator.js:79-122) and billed.Client:
apiPersonalities.generateAgentPersonality(client/src/services/apiPersonalities.js:21-23) posts{ seed, providerId, model };AgentList.jsx:90-104always sends a structured object, so the UI is fine — the route is not.POST
/api/agents/personalities/:id/toggle(server/routes/agentPersonalities.js:84-88) passesreq.body.enabledstraight intotoggleAgent→updateAgent(id, { enabled })(server/services/agentPersonalities.js:140-141) with no boolean check. A string"false"is truthy and persists asenabled: "false". Later UI!agent.enabled(client/src/components/agents/AgentDetail.jsx:70) then mis-inverts. Create/update already requireenabled: z.boolean().GET
/(server/routes/agentPersonalities.js:17-24) takesreq.query.userIdwith no schema. An array query (?userId=a&userId=b) reachesgetAgentsByUser.Impact
VALIDATION_ERROR, and skip the{ error, code, timestamp }envelope the rest of this router uses viaServerError.source: 'agent-personality-generation').enabled, so the next click and anyif (agent.enabled)gate read the wrong consent state.Fix
Add two small schemas next to
agentSchemainserver/lib/agentValidation.js(or inline in the route) and wirevalidateRequest:agentGenerateSchema:{ seed: agentSchema.partial().optional().default({}), providerId: z.string().min(1).max(128).nullable().optional(), model: z.string().min(1).max(300).nullable().optional() }.strict(). Rejectseed: null(do not coerce). Nestedpersonality/avatarmust be objects or omitted, notnull.agentToggleSchema:{ enabled: z.boolean() }.strict().z.object({ userId: z.string().min(1).max(100).optional() })on GET/.Do not change the generate prompt or the stored personality shape. Keep throwing
ServerErrorfrom the service for provider-unavailable; only the request body becomes 400.Scope
small
Acceptance criteria
POST /api/agents/personalities/generatewithseed: null,seed: { personality: null }, orseed: { name: 123 }returns 400VALIDATION_ERROR, not 500.agentSchema/agentPersonalitySchema.POST /api/agents/personalities/:id/togglewith missing or non-booleanenabledreturns 400; only a real boolean is persisted.agentSchema/agentUpdateSchemaunchanged.