meta.ts 4.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. /**
  2. * Meta validation: check the caller-provided {@link WorkflowMeta} DATA against the shape
  3. * contract and reject everything else loud, every violation named. Meta arrives as schema-checked
  4. * JSON data, never evaluated script text; evaluating it on the host could run getters outside the
  5. * worker timeout that exists to isolate model-written code.
  6. * @module @deepseek-ai/dsh-workflow-workerthread/meta
  7. */
  8. import { WorkflowError } from '@deepseek-ai/dsh-workflow'
  9. import type { WorkflowMeta, WorkflowPhase } from '@deepseek-ai/dsh-workflow'
  10. /** Collect shape violations for a meta value (plain JSON data by the seam contract). */
  11. function validateMetaShape(meta: unknown): { meta?: WorkflowMeta; violations: string[] } {
  12. const violations: string[] = []
  13. if (typeof meta !== 'object' || meta === null || Array.isArray(meta)) {
  14. return { violations: ['meta must be an object'] }
  15. }
  16. const record = meta as Record<string, unknown>
  17. const known = new Set(['name', 'description', 'whenToUse', 'phases'])
  18. for (const key of Object.keys(record)) {
  19. if (!known.has(key)) violations.push(`meta.${key} is not a recognized field (name/description/whenToUse/phases)`)
  20. }
  21. if (typeof record.name !== 'string' || record.name.length === 0) violations.push('meta.name must be a non-empty string')
  22. if (typeof record.description !== 'string' || record.description.length === 0) violations.push('meta.description must be a non-empty string')
  23. if (record.whenToUse !== undefined && typeof record.whenToUse !== 'string') violations.push('meta.whenToUse must be a string')
  24. const phases: WorkflowPhase[] = []
  25. if (record.phases !== undefined) {
  26. if (!Array.isArray(record.phases)) {
  27. violations.push('meta.phases must be an array')
  28. } else {
  29. record.phases.forEach((phase, index) => {
  30. if (typeof phase !== 'object' || phase === null || Array.isArray(phase)) {
  31. violations.push(`meta.phases[${index}] must be an object`)
  32. return
  33. }
  34. const entry = phase as Record<string, unknown>
  35. for (const key of Object.keys(entry)) {
  36. if (!['title', 'detail', 'provider', 'model'].includes(key)) violations.push(`meta.phases[${index}].${key} is not a recognized field`)
  37. }
  38. if (typeof entry.title !== 'string' || entry.title.length === 0) violations.push(`meta.phases[${index}].title must be a non-empty string`)
  39. if (entry.detail !== undefined && typeof entry.detail !== 'string') violations.push(`meta.phases[${index}].detail must be a string`)
  40. if (entry.provider !== undefined && typeof entry.provider !== 'string') violations.push(`meta.phases[${index}].provider must be a string`)
  41. if (entry.model !== undefined && typeof entry.model !== 'string') violations.push(`meta.phases[${index}].model must be a string`)
  42. if (violations.length === 0) {
  43. phases.push({
  44. title: entry.title as string,
  45. ...entry.detail !== undefined ? { detail: entry.detail as string } : {},
  46. ...entry.provider !== undefined ? { provider: entry.provider as string } : {},
  47. ...entry.model !== undefined ? { model: entry.model as string } : {},
  48. })
  49. }
  50. })
  51. }
  52. }
  53. if (violations.length > 0) return { violations }
  54. return {
  55. violations,
  56. meta: {
  57. name: record.name as string,
  58. description: record.description as string,
  59. ...record.whenToUse !== undefined ? { whenToUse: record.whenToUse as string } : {},
  60. ...record.phases !== undefined ? { phases } : {},
  61. },
  62. }
  63. }
  64. /**
  65. * Validate a caller-provided meta value against the {@link WorkflowMeta}
  66. * contract. Throws `META_INVALID` naming every violation (unknown fields,
  67. * missing/mistyped `name`/`description`, malformed `phases`); the returned
  68. * meta is a NORMALIZED copy built from the validated fields, so the engine
  69. * never aliases the caller's object.
  70. * @param value - the meta data from the start request (plain JSON by the seam contract).
  71. * @returns the validated, normalized meta block.
  72. */
  73. export function validateMeta(value: unknown): WorkflowMeta {
  74. const { meta, violations } = validateMetaShape(value)
  75. if (meta === undefined) {
  76. throw new WorkflowError(`invalid meta: ${violations.join('; ')}`, 'META_INVALID')
  77. }
  78. return meta
  79. }