What
POST /api/plugins/skills in server/src/plugins/routes.ts:568-588 checks presence with !body?.slug and then runs /.../.test(body.slug).
RegExp.test coerces, so {"slug":123,...} tests the string "123" and passes validation. summary has no type check at all (body.summary ?? ""), so {"summary":{}} reaches store.installSkill and the Drizzle/PG insert throws an uncaught error — a 500 for a caller error.
Siblings on the same router (POST /grants, POST /call) already require typeof === "string" + .trim() and answer 400.
Repro
POST /api/plugins/skills with {"slug":123,"title":"t","instructions":"i"} → passes validation, 500 from the store insert.
POST /api/plugins/skills with {"slug":"ok-slug","title":"t","instructions":"i","summary":{}} → passes validation, 500 from the store insert.
Expected: 400 naming the field, no DB write, matching the grants/call routes.
Where it runs
Stateless request validation in the server process. Same 400 on every replica; no shared state, no fan-out, no new listener/schedule.
Fix sketch
Require typeof slug === "string" before the regex, require summary === undefined || typeof summary === "string" (and trim title/instructions already imply strings), return 400 otherwise with the existing message shape. Add route tests covering numeric slug and object/array summary.
What
POST /api/plugins/skillsinserver/src/plugins/routes.ts:568-588checks presence with!body?.slugand then runs/.../.test(body.slug).RegExp.testcoerces, so{"slug":123,...}tests the string"123"and passes validation.summaryhas no type check at all (body.summary ?? ""), so{"summary":{}}reachesstore.installSkilland the Drizzle/PG insert throws an uncaught error — a 500 for a caller error.Siblings on the same router (
POST /grants,POST /call) already requiretypeof === "string"+.trim()and answer 400.Repro
POST /api/plugins/skillswith{"slug":123,"title":"t","instructions":"i"}→ passes validation, 500 from the store insert.POST /api/plugins/skillswith{"slug":"ok-slug","title":"t","instructions":"i","summary":{}}→ passes validation, 500 from the store insert.Expected: 400 naming the field, no DB write, matching the grants/call routes.
Where it runs
Stateless request validation in the server process. Same 400 on every replica; no shared state, no fan-out, no new listener/schedule.
Fix sketch
Require
typeof slug === "string"before the regex, requiresummary === undefined || typeof summary === "string"(and trim title/instructions already imply strings), return 400 otherwise with the existing message shape. Add route tests covering numeric slug and object/array summary.