Перейти к содержимому

Система валидации

Система валидации

Moira выполняет всестороннюю валидацию на нескольких уровнях для обеспечения целостности workflow и корректности ответов агентов.

Уровни валидации

1. Валидация JSON Schema

Workflow валидируются против определения JSON Schema:

  • Валидация структуры против схемы workflow
  • Проверка обязательных полей (id, metadata, nodes)
  • Валидация типов для всех свойств
  • Ветви схемы встроенных типов и форма типа расширения с пространством имён

2. Структурная валидация

Структура графа анализируется на корректность:

  • Связность нод - Все connections указывают на валидные ноды
  • Обязательные ноды - Нода start должна существовать
  • Циклические зависимости - Циклы обнаруживаются и помечаются
  • Недостижимые ноды - Ноды, к которым не ведёт ни одна точка входа. Точки входа — нода start и каждый teleport: у телепорта нет обычных входящих связей, поэтому всё, куда он ведёт, достижимо через step({ teleportTo }) и в предупреждение не попадает
  • Записи реестра - Каждая запись variableRegistry должна быть валидной JSON Schema (некорректный items/pattern/и т.п. — blocking-ошибка) с непустым описанием
  • Объявления materialize - У каждой записи ровно один источник (from или пустой content), источники реестра имеют строковый default, объявленные пути безопасны и уникальны, а все цели соединений существуют
  • Блоки процесса - Если progress присутствует, каждая нода объявляет progressNodeId существующего блока, у каждого блока есть content.summary, каждая связь, выходящая из блока или возвращающаяся назад, несёт запись connectionLabels (у возвратов — с cycle.cause и cycle.exit), а каждый шаблон {{progress_*_outcome}} стоит на одном блоке, владеющем записывающей нодой; каждое нарушение — ошибка со стабильным кодом (unowned-node, unknown-block, empty-block, empty-description, unlabeled-edge, unexplained-cycle, outcome-duplicate, outcome-unowned, unconnected-block)
  • Типы узлов расширений - Для типов из live-реестра расширений или опубликованного snapshot проверяется объявленная ими схема конфигурации. Если live-реестр не содержит тип, это ошибка; если типа нет в snapshot или доступных данных реестра нет, валидатор выдаёт предупреждение о невозможности разрешить тип

3. Валидация ввода

Ответы агентов валидируются против inputSchema:

  • Валидация JSON Schema на основе AJV
  • Проверка типов для полей ответа
  • Валидация обязательных полей
  • Валидация pattern и format

Результаты валидации

Валидация возвращает структурированные результаты:

{
valid: boolean;
errors: ValidationError[];
warnings: ValidationWarning[];
}

Типы ошибок

ТипОписаниеПример
schemaНевалидная JSON структураОтсутствует обязательное поле
structureНевалидная структура графаOrphan нода
connectionНевалидное соединениеУказывает на несуществующую ноду
referenceНевалидная ссылкаНевалидный subgraph ID

Типы предупреждений

ТипОписаниеПорог
performanceБольшой workflow>20 agent-directive нод
complexityСложные условияГлубоко вложенные условия
contextБольшой контекст>100KB размер контекста

Примеры валидации

Валидный Workflow

{
"id": "valid-workflow",
"metadata": {
"name": "Valid Workflow",
"version": "1.0.0",
"description": "A valid workflow"
},
"nodes": [
{ "id": "start", "type": "start", "connections": { "default": "task" } },
{
"id": "task",
"type": "agent-directive",
"directive": "...",
"completionCondition": "...",
"connections": { "success": "end" }
},
{ "id": "end", "type": "end" }
]
}

Результат:

{ "valid": true, "errors": [], "warnings": [] }

Невалидный Workflow - Отсутствует соединение

{
"nodes": [
{ "id": "start", "type": "start", "connections": { "default": "missing" } },
{ "id": "end", "type": "end" }
]
}

Результат:

{
"valid": false,
"errors": [
{
"type": "connection",
"message": "Node 'start' references non-existent node 'missing'",
"nodeId": "start"
}
]
}

Workflow с предупреждением

Большой workflow вызывает предупреждение о производительности:

{
"valid": true,
"errors": [],
"warnings": [
{
"type": "performance",
"message": "Workflow has 25 agent-directive nodes. Consider breaking into subgraphs.",
"count": 25
}
]
}

Валидация Input Schema

Ответы агентов валидируются против inputSchema определенной на agent-directive нодах.

Ноды без inputSchema

Ноды без inputSchema требуют пустой ввод от агента. Непустые ответы отклоняются:

// Нода без inputSchema
{ "id": "task", "type": "agent-directive", "directive": "..." }
// Валидно: пустой ответ
{}
// Невалидно: непустой ответ
{ "result": "done" } // Отклоняется с ошибкой валидации

Определение схемы

{
"type": "agent-directive",
"inputSchema": {
"type": "object",
"properties": {
"result": { "type": "string" },
"confidence": { "type": "number", "minimum": 0, "maximum": 10 }
},
"required": ["result"]
}
}

Валидный ответ

{ "result": "completed", "confidence": 8 }

Невалидный ответ

{ "confidence": "high" }

Ошибка:

{
"valid": false,
"errors": [
{ "field": "result", "message": "Required field missing" },
{ "field": "confidence", "message": "Expected number, got string" }
]
}

Предупреждение о переменной, объявленной без default

Валидатор выдаёт warning (severity warning, не ошибка — workflow остаётся валидным), когда переменная из variableRegistry используется в directive, completionCondition, message или condition, но у переменной нет default и её не записывает ни один вышестоящий узел через globalInputs (и она отсутствует в initialData стартового узла).

В рантайме такая ссылка отображается как литеральный плейсхолдер [[UNDEFINED_VARIABLE]] вместо значения.

{
"valid": true,
"errors": [],
"warnings": [
{
"type": "structure",
"severity": "warning",
"nodeId": "do-work",
"message": "Variable 'iteration' is referenced in node 'do-work' but has no default and is never written by an upstream node. It will render [[UNDEFINED_VARIABLE]] at runtime."
}
]
}

Исправить можно одним из двух способов:

  • Добавить default для переменной в variableRegistry.
  • Сделать так, чтобы вышестоящий узел записывал переменную через globalInputs до узла, который на неё ссылается.

Валидация ссылок на playbook

Определение, называющее playbook через {{playbook:name}}, принимается только если автор может его прочитать: это либо его собственный playbook, либо опубликованный playbook другого аккаунта. Недоступная ссылка — блокирующая ошибка на любом пути записи определения: инструмент manage отказывает в создании и в правке, а сохранённое через API определение остаётся невалидным. Сообщение называет playbook и три выхода: создать его, опубликовать или убрать ссылку.

То же правило применяется ещё раз при старте запуска, потому что playbook может исчезнуть между последней правкой и стартом. start отказывает до создания запуска, а не показывает шаг, в котором текст поведения отсутствует.

Ссылка с ведущим обратным слэшем (\{{playbook:name}}) — литеральный текст, и она не проверяется: так документ объясняет синтаксис, не называя ни одного playbook.

Безопасность от инъекций

Подставленные ЗНАЧЕНИЯ переменных и данных никогда не выполняются повторно как шаблоны. Когда движок подставляет значение в directive или message, это значение трактуется как литеральная строка — синтаксис фигурных скобок, пришедший из подставленных данных, нейтрализуется и не парсится повторно.

Это означает, что шаблоны выполняются только в статических полях узла, контролируемых автором, а не в значениях, которые приходят из ввода агента или внешних данных.

Содержимое playbook подчиняется той же границе. Собственный playbook — авторский текст, и он обрабатывается как остальная часть определения; playbook другого аккаунта нейтрализуется перед подстановкой, поэтому опубликованный текст не может выполнить шаблоны внутри вашего запуска.

Не подставляйте недоверенный ввод, содержащий {{...}}, в директиву. Объявляйте нужные переменные и ссылайтесь на них явно. Предпочитайте явные именованные переменные вместо полных дампов {{context.variables}}, которые могут раскрыть всю коллекцию переменных.

Валидация Materialize

Нода materialize допустима, только если basePath непустой, files содержит от 1 до 100 записей, а connections.success задан. Каждый объявленный files[].path должен быть нормализованным относительным путём без NUL, абсолютного или начинающегося с обратной косой черты корня, пустых сегментов, . и ..; объявленные пути должны быть уникальны. Каждая запись задаёт ровно одно из:

  • from: имя записи variableRegistry с type: "string" и строковым default;
  • content: "": пустой каркасный файл. Непустой inline-контент отклоняется.

Поскольку пути могут содержать шаблоны, те же проверки безопасности и коллизий повторяются после рендеринга с текущим контекстом выполнения. Рендеринг также ограничивает каждый файл 1 MiB, а всё несжатое содержимое — 10 MiB. Ошибка подготовки шага переходит по необязательному connections.error, а без него выбрасывается. Каждая HTTP-загрузка заново проверяет пятиминутное разрешение и его привязку к пользователю, выполнению и ноде; повторы принимаются, только пока привязанное выполнение ожидает на этой ноде. Доставка в контекст проверяет те же привязки и добавляет ещё одно ограничение: набор суммарно больше 256 KiB отклоняется, а не усекается.

Лучшие практики

  1. Всегда включайте inputSchema - Валидируйте ответы агентов для консистентных данных
  2. Держите workflow сфокусированными - Разбивайте большие workflow на subgraphs
  3. Тестируйте валидацию - Используйте manage с includeValidation: true
  4. Обрабатывайте ошибки gracefully - Определяйте error connections для ошибок валидации