Система валидации
Система валидации
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 отклоняется, а не усекается.
Лучшие практики
- Всегда включайте inputSchema - Валидируйте ответы агентов для консистентных данных
- Держите workflow сфокусированными - Разбивайте большие workflow на subgraphs
- Тестируйте валидацию - Используйте
manageсincludeValidation: true - Обрабатывайте ошибки gracefully - Определяйте error connections для ошибок валидации