meta.ts 4.2 KB

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